diff options
author | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
---|---|---|
committer | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
commit | 3d8ce1a11a7347cc94a937719c4bc8df46fb8d14 (patch) | |
tree | 8c26ca375a6312751c00a27e1653fb6f189f0463 /browser/devtools/webconsole | |
parent | e449bdb1ec3a82f204bffdd9c3c54069d086eee3 (diff) | |
download | palemoon-gre-3d8ce1a11a7347cc94a937719c4bc8df46fb8d14.tar.gz |
Base import of Tycho code (warning: huge commit)
Diffstat (limited to 'browser/devtools/webconsole')
328 files changed, 26375 insertions, 1422 deletions
diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in deleted file mode 100644 index 0bd782f05..000000000 --- a/browser/devtools/webconsole/Makefile.in +++ /dev/null @@ -1,21 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DEPTH = @DEPTH@ -topsrcdir = @top_srcdir@ -srcdir = @srcdir@ -VPATH = @srcdir@ - -include $(DEPTH)/config/autoconf.mk - -EXTRA_JS_MODULES = \ - HUDService.jsm \ - NetworkPanel.jsm \ - WebConsolePanel.jsm \ - $(NULL) - -include $(topsrcdir)/config/rules.mk - -libs:: - $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/framework diff --git a/browser/devtools/webconsole/console-commands.js b/browser/devtools/webconsole/console-commands.js new file mode 100644 index 000000000..471362c08 --- /dev/null +++ b/browser/devtools/webconsole/console-commands.js @@ -0,0 +1,88 @@ +/* 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 gcli = require("gcli/index"); +const { gDevTools } = require("resource:///modules/devtools/gDevTools.jsm"); + +exports.items = [ + { + name: 'splitconsole', + hidden: true, + buttonId: "command-button-splitconsole", + buttonClass: "command-button command-button-invertable", + tooltipText: gcli.lookup("splitconsoleTooltip"), + isRemoteSafe: true, + state: { + isChecked: function(target) { + let toolbox = gDevTools.getToolbox(target); + return !!(toolbox && toolbox.splitConsole); + }, + onChange: function(target, changeHandler) { + // Register handlers for when a change event should be fired + // (which resets the checked state of the button). + let toolbox = gDevTools.getToolbox(target); + let callback = changeHandler.bind(null, "changed", { target: target }); + + if (!toolbox) { + return; + } + + toolbox.on("split-console", callback); + toolbox.once("destroyed", () => { + toolbox.off("split-console", callback); + }); + } + }, + exec: function(args, context) { + let target = context.environment.target; + let toolbox = gDevTools.getToolbox(target); + + if (!toolbox) { + return gDevTools.showToolbox(target, "inspector").then((toolbox) => { + toolbox.toggleSplitConsole(); + }); + } else { + toolbox.toggleSplitConsole(); + } + } + }, + { + name: "console", + description: gcli.lookup("consoleDesc"), + manual: gcli.lookup("consoleManual") + }, + { + name: "console clear", + description: gcli.lookup("consoleclearDesc"), + exec: function(args, context) { + let toolbox = gDevTools.getToolbox(context.environment.target); + if (toolbox == null) { + return; + } + + let panel = toolbox.getPanel("webconsole"); + if (panel == null) { + return; + } + + panel.hud.jsterm.clearOutput(); + } + }, + { + name: "console close", + description: gcli.lookup("consolecloseDesc"), + exec: function(args, context) { + return gDevTools.closeToolbox(context.environment.target); + } + }, + { + name: "console open", + description: gcli.lookup("consoleopenDesc"), + exec: function(args, context) { + return gDevTools.showToolbox(context.environment.target, "webconsole"); + } + } +]; diff --git a/browser/devtools/webconsole/console-output.js b/browser/devtools/webconsole/console-output.js new file mode 100644 index 000000000..f78e24cbd --- /dev/null +++ b/browser/devtools/webconsole/console-output.js @@ -0,0 +1,3567 @@ +/* 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 {Cc, Ci, Cu} = require("chrome"); + +loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); +loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm"); +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); +loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm"); +loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); +loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm"); + +loader.lazyRequireGetter(this, "promise"); +loader.lazyRequireGetter(this, "TableWidget", "devtools/shared/widgets/TableWidget", true); + +const Heritage = require("sdk/core/heritage"); +const URI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; + +const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; +const l10n = new WebConsoleUtils.l10n(STRINGS_URI); + +const MAX_STRING_GRIP_LENGTH = 36; +const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + +// 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 preference keys to use for each category/severity combination, indexed + // first by category (rows) and then by severity (columns). + // + // Most of these rather idiosyncratic names are historical and predate the + // division of message type into "category" and "severity". + PREFERENCE_KEYS: [ + // Error Warning Info Log + [ "network", "netwarn", null, "networkinfo", ], // Network + [ "csserror", "cssparser", null, null, ], // CSS + [ "exception", "jswarn", null, "jslog", ], // JS + [ "error", "warn", "info", "log", ], // Web Developer + [ null, null, null, null, ], // Input + [ null, null, null, null, ], // Output + [ "secerror", "secwarn", null, null, ], // Security + ], + + // The fragment of a CSS class name that identifies each category. + CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console", + "input", "output", "security" ], + + // The fragment of a CSS class name that identifies each severity. + SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ], + + // The indent of a console group in pixels. + GROUP_INDENT: 12, +}; + +// A map from the console API call levels to the Web Console severities. +const CONSOLE_API_LEVELS_TO_SEVERITIES = { + error: "error", + exception: "error", + assert: "error", + warn: "warning", + info: "info", + log: "log", + trace: "log", + table: "log", + debug: "log", + dir: "log", + group: "log", + groupCollapsed: "log", + groupEnd: "log", + time: "log", + timeEnd: "log", + count: "log" +}; + +// Array of known message source URLs we need to hide from output. +const IGNORED_SOURCE_URLS = ["debugger eval code"]; + +// The maximum length of strings to be displayed by the Web Console. +const MAX_LONG_STRING_LENGTH = 200000; + +// Regular expression that matches the allowed CSS property names when using +// the `window.console` API. +const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/; + +// Regular expressions to search and replace with 'notallowed' in the styles +// given to the `window.console` API methods. +const RE_CLEANUP_STYLES = [ + // url(), -moz-element() + /\b(?:url|(?:-moz-)?element)[\s('"]+/gi, + + // various URL protocols + /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi, +]; + +// Maximum number of rows to display in console.table(). +const TABLE_ROW_MAX_ITEMS = 1000; + +// Maximum number of columns to display in console.table(). +const TABLE_COLUMN_MAX_ITEMS = 10; + +/** + * 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 = { + _dummyElement: null, + + /** + * The output container. + * @type DOMElement + */ + get element() { + return this.owner.outputNode; + }, + + /** + * The document that holds the output. + * @type DOMDocument + */ + get document() { + return this.owner ? this.owner.document : null; + }, + + /** + * The DOM window that holds the output. + * @type Window + */ + get window() { + return this.owner.window; + }, + + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() { + return this.owner.webConsoleClient; + }, + + /** + * Getter for the current toolbox debuggee target. + * @type Target + */ + get toolboxTarget() { + return this.owner.owner.target; + }, + + /** + * Release an actor. + * + * @private + * @param string actorId + * The actor ID you want to release. + */ + _releaseObject: function(actorId) + { + this.owner._releaseObject(actorId); + }, + + /** + * 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; + }, + + /** + * Get an array of selected messages. This list is based on the text selection + * start and end points. + * + * @param number [limit] + * Optional limit of selected messages you want. If no value is given, + * all of the selected messages are returned. + * @return array + * Array of DOM elements for each message that is currently selected. + */ + getSelectedMessages: function(limit) + { + let selection = this.window.getSelection(); + if (selection.isCollapsed) { + return []; + } + + if (selection.containsNode(this.element, true)) { + return Array.slice(this.element.children); + } + + let anchor = this.getMessageForElement(selection.anchorNode); + let focus = this.getMessageForElement(selection.focusNode); + if (!anchor || !focus) { + return []; + } + + let start, end; + if (anchor.timestamp > focus.timestamp) { + start = focus; + end = anchor; + } else { + start = anchor; + end = focus; + } + + let result = []; + let current = start; + while (current) { + result.push(current); + if (current == end || (limit && result.length == limit)) { + break; + } + current = current.nextSibling; + } + return result; + }, + + /** + * Find the DOM element of a message for any given descendant. + * + * @param DOMElement elem + * The element to start the search from. + * @return DOMElement|null + * The DOM element of the message, if any. + */ + getMessageForElement: function(elem) + { + while (elem && elem.parentNode) { + if (elem.classList && elem.classList.contains("message")) { + return elem; + } + elem = elem.parentNode; + } + return null; + }, + + /** + * Select all messages. + */ + selectAllMessages: function() + { + let selection = this.window.getSelection(); + selection.removeAllRanges(); + let range = this.document.createRange(); + range.selectNodeContents(this.element); + selection.addRange(range); + }, + + /** + * Add a message to the selection. + * + * @param DOMElement elem + * The message element to select. + */ + selectMessage: function(elem) + { + let selection = this.window.getSelection(); + selection.removeAllRanges(); + let range = this.document.createRange(); + range.selectNodeContents(elem); + selection.addRange(range); + }, + + /** + * Open an URL in a new tab. + * @see WebConsole.openLink() in hudservice.js + */ + openLink: function() + { + this.owner.owner.openLink.apply(this.owner.owner, arguments); + }, + + /** + * Open the variables view to inspect an object actor. + * @see JSTerm.openVariablesView() in webconsole.js + */ + openVariablesView: function() + { + this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments); + }, + + /** + * Destroy this ConsoleOutput instance. + */ + destroy: function() + { + this._dummyElement = null; + 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(); + this._onClickAnchor = this._onClickAnchor.bind(this); + this._repeatID = { uid: gSequenceId() }; + this.textContent = ""; +}; + +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; + }, + + /** + * The owner DOM document. + * @type DOMElement + */ + get document() { + return this.output.document; + }, + + /** + * Holds the text-only representation of the message. + * @type string + */ + textContent: null, + + /** + * Set of widgets included in this message. + * @type Set + */ + widgets: null, + + // Properties that allow compatibility with the current Web Console output + // implementation. + _categoryCompat: null, + _severityCompat: null, + _categoryNameCompat: null, + _severityNameCompat: null, + _filterKeyCompat: null, + + /** + * Object that is JSON-ified and used as a non-unique ID for tracking + * duplicate messages. + * @private + * @type object + */ + _repeatID: 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; + }, + + /** + * Non-unique ID for this message object used for tracking duplicate messages. + * Different message kinds can identify themselves based their own criteria. + * + * @return string + */ + getRepeatID: function() + { + return JSON.stringify(this._repeatID); + }, + + /** + * 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 is fixed. + * + * @private + * @return Element + * The DOM element that wraps the message. + */ + _renderCompat: function() + { + let doc = this.output.document; + let container = doc.createElementNS(XHTML_NS, "div"); + container.id = "console-msg-" + gSequenceId(); + container.className = "message"; + if (this.category == "input") { + // Assistive technology tools shouldn't echo input to the user, + // as the user knows what they've just typed. + container.setAttribute("aria-live", "off"); + } + container.category = this._categoryCompat; + container.severity = this._severityCompat; + container.setAttribute("category", this._categoryNameCompat); + container.setAttribute("severity", this._severityNameCompat); + container.setAttribute("filter", this._filterKeyCompat); + container.clipboardText = this.textContent; + container.timestamp = this.timestamp; + container._messageObject = this; + + return container; + }, + + /** + * Add a click callback to a given DOM element. + * + * @private + * @param Element element + * The DOM element to which you want to add a click event handler. + * @param function [callback=this._onClickAnchor] + * Optional click event handler. The default event handler is + * |this._onClickAnchor|. + */ + _addLinkCallback: function(element, callback = this._onClickAnchor) + { + // This is going into the WebConsoleFrame object instance that owns + // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole + // object instance from hudservice.js. + // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766 + // is fixed. + this.output.owner._addMessageLinkCallback(element, callback); + }, + + /** + * The default |click| event handler for links in the output. This function + * opens the anchor's link in a new tab. + * + * @private + * @param Event event + * The DOM event that invoked this function. + */ + _onClickAnchor: function(event) + { + this.output.openLink(event.target.href); + }, + + destroy: function() + { + // Destroy all widgets that have registered themselves in this.widgets + for (let widget of this.widgets) { + widget.destroy(); + } + this.widgets.clear(); + } +}; // Messages.BaseMessage.prototype + + +/** + * The NavigationMarker is used to show a page load event. + * + * @constructor + * @extends Messages.BaseMessage + * @param object response + * The response received from the back end. + * @param number timestamp + * The message date and time, milliseconds elapsed since 1 January 1970 + * 00:00:00 UTC. + */ +Messages.NavigationMarker = function(response, timestamp) +{ + Messages.BaseMessage.call(this); + + // Store the response packet received from the server. It might + // be useful for extensions customizing the console output. + this.response = response; + this._url = response.url; + this.textContent = "------ " + this._url; + this.timestamp = timestamp; +}; + +Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype, +{ + /** + * The address of the loading page. + * @private + * @type string + */ + _url: null, + + /** + * Message timestamp. + * + * @type number + * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. + */ + timestamp: 0, + + _categoryCompat: COMPAT.CATEGORIES.NETWORK, + _severityCompat: COMPAT.SEVERITIES.LOG, + _categoryNameCompat: "network", + _severityNameCompat: "info", + _filterKeyCompat: "networkinfo", + + /** + * 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, "a"); + urlnode.className = "url"; + urlnode.textContent = url; + urlnode.title = this._url; + urlnode.href = this._url; + urlnode.draggable = false; + this._addLinkCallback(urlnode); + + let render = Messages.BaseMessage.prototype.render.bind(this); + render().element.appendChild(urlnode); + this.element.classList.add("navigation-marker"); + this.element.url = this._url; + this.element.appendChild(doc.createTextNode("\n")); + + return this; + }, +}); // Messages.NavigationMarker.prototype + + +/** + * The Simple message is used to show any basic message in the Web Console. + * + * @constructor + * @extends Messages.BaseMessage + * @param string|Node|function message + * The message to display. + * @param object [options] + * Options for this message: + * - category: (string) category that this message belongs to. Defaults + * to no category. + * - severity: (string) severity of the message. Defaults to no severity. + * - timestamp: (number) date and time when the message was recorded. + * Defaults to |Date.now()|. + * - link: (string) if provided, the message will be wrapped in an anchor + * pointing to the given URL here. + * - linkCallback: (function) if provided, the message will be wrapped in + * an anchor. The |linkCallback| function will be added as click event + * handler. + * - location: object that tells the message source: url, line, column + * and lineText. + * - className: (string) additional element class names for styling + * purposes. + * - private: (boolean) mark this as a private message. + * - filterDuplicates: (boolean) true if you do want this message to be + * filtered as a potential duplicate message, false otherwise. + */ +Messages.Simple = function(message, options = {}) +{ + Messages.BaseMessage.call(this); + + this.category = options.category; + this.severity = options.severity; + this.location = options.location; + this.timestamp = options.timestamp || Date.now(); + this.private = !!options.private; + + this._message = message; + this._className = options.className; + this._link = options.link; + this._linkCallback = options.linkCallback; + this._filterDuplicates = options.filterDuplicates; +}; + +Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype, +{ + /** + * Message category. + * @type string + */ + category: null, + + /** + * Message severity. + * @type string + */ + severity: null, + + /** + * Message source location. Properties: url, line, column, lineText. + * @type object + */ + location: null, + + /** + * Tells if this message comes from a private browsing context. + * @type boolean + */ + private: false, + + /** + * Custom class name for the DOM element of the message. + * @private + * @type string + */ + _className: null, + + /** + * Message link - if this message is clicked then this URL opens in a new tab. + * @private + * @type string + */ + _link: null, + + /** + * Message click event handler. + * @private + * @type function + */ + _linkCallback: null, + + /** + * Tells if this message should be checked if it is a duplicate of another + * message or not. + */ + _filterDuplicates: false, + + /** + * The raw message displayed by this Message object. This can be a function, + * DOM node or a string. + * + * @private + * @type mixed + */ + _message: null, + + _afterMessage: null, + _objectActors: null, + _groupDepthCompat: 0, + + /** + * Message timestamp. + * + * @type number + * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. + */ + timestamp: 0, + + get _categoryCompat() { + return this.category ? + COMPAT.CATEGORIES[this.category.toUpperCase()] : null; + }, + get _severityCompat() { + return this.severity ? + COMPAT.SEVERITIES[this.severity.toUpperCase()] : null; + }, + get _categoryNameCompat() { + return this.category ? + COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null; + }, + get _severityNameCompat() { + return this.severity ? + COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null; + }, + + get _filterKeyCompat() { + return this._categoryCompat !== null && this._severityCompat !== null ? + COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] : + null; + }, + + init: function() + { + Messages.BaseMessage.prototype.init.apply(this, arguments); + this._groupDepthCompat = this.output.owner.groupDepth; + this._initRepeatID(); + return this; + }, + + _initRepeatID: function() + { + if (!this._filterDuplicates) { + return; + } + + // Add the properties we care about for identifying duplicate messages. + let rid = this._repeatID; + delete rid.uid; + + rid.category = this.category; + rid.severity = this.severity; + rid.private = this.private; + rid.location = this.location; + rid.link = this._link; + rid.linkCallback = this._linkCallback + ""; + rid.className = this._className; + rid.groupDepth = this._groupDepthCompat; + rid.textContent = ""; + }, + + getRepeatID: function() + { + // No point in returning a string that includes other properties when there + // is a unique ID. + if (this._repeatID.uid) { + return JSON.stringify({ uid: this._repeatID.uid }); + } + + return JSON.stringify(this._repeatID); + }, + + render: function() + { + if (this.element) { + return this; + } + + let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render(); + + let icon = this.document.createElementNS(XHTML_NS, "span"); + icon.className = "icon"; + icon.title = l10n.getStr("severity." + this._severityNameCompat); + + // Apply the current group by indenting appropriately. + // TODO: remove this once bug 778766 is fixed. + let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT; + let indentNode = this.document.createElementNS(XHTML_NS, "span"); + indentNode.className = "indent"; + indentNode.style.width = indent + "px"; + + let body = this._renderBody(); + this._repeatID.textContent += "|" + body.textContent; + + let repeatNode = this._renderRepeatNode(); + let location = this._renderLocation(); + + Messages.BaseMessage.prototype.render.call(this); + if (this._className) { + this.element.className += " " + this._className; + } + + this.element.appendChild(timestamp.element); + this.element.appendChild(indentNode); + this.element.appendChild(icon); + this.element.appendChild(body); + if (repeatNode) { + this.element.appendChild(repeatNode); + } + if (location) { + this.element.appendChild(location); + } + this.element.appendChild(this.document.createTextNode("\n")); + + this.element.clipboardText = this.element.textContent; + + if (this.private) { + this.element.setAttribute("private", true); + } + + if (this._afterMessage) { + this.element._outputAfterNode = this._afterMessage.element; + this._afterMessage = null; + } + + // TODO: handle object releasing in a more elegant way once all console + // messages use the new API - bug 778766. + this.element._objectActors = this._objectActors; + this._objectActors = null; + + return this; + }, + + /** + * Render the message body DOM element. + * @private + * @return Element + */ + _renderBody: function() + { + let body = this.document.createElementNS(XHTML_NS, "span"); + body.className = "message-body-wrapper message-body devtools-monospace"; + + let bodyInner = this.document.createElementNS(XHTML_NS, "span"); + body.appendChild(bodyInner); + + let anchor, container = bodyInner; + if (this._link || this._linkCallback) { + container = anchor = this.document.createElementNS(XHTML_NS, "a"); + anchor.href = this._link || "#"; + anchor.draggable = false; + this._addLinkCallback(anchor, this._linkCallback); + bodyInner.appendChild(anchor); + } + + if (typeof this._message == "function") { + container.appendChild(this._message(this)); + } else if (this._message instanceof Ci.nsIDOMNode) { + container.appendChild(this._message); + } else { + container.textContent = this._message; + } + + return body; + }, + + /** + * Render the repeat bubble DOM element part of the message. + * @private + * @return Element + */ + _renderRepeatNode: function() + { + if (!this._filterDuplicates) { + return null; + } + + let repeatNode = this.document.createElementNS(XHTML_NS, "span"); + repeatNode.setAttribute("value", "1"); + repeatNode.className = "message-repeats"; + repeatNode.textContent = 1; + repeatNode._uid = this.getRepeatID(); + return repeatNode; + }, + + /** + * Render the message source location DOM element. + * @private + * @return Element + */ + _renderLocation: function() + { + if (!this.location) { + return null; + } + + let {url, line, column} = this.location; + if (IGNORED_SOURCE_URLS.indexOf(url) != -1) { + return null; + } + + // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js. + // TODO: move createLocationNode() into this file when bug 778766 is fixed. + return this.output.owner.createLocationNode({url: url, + line: line, + column: column}); + }, +}); // Messages.Simple.prototype + + +/** + * The Extended message. + * + * @constructor + * @extends Messages.Simple + * @param array messagePieces + * The message to display given as an array of elements. Each array + * element can be a DOM node, function, ObjectActor, LongString or + * a string. + * @param object [options] + * Options for rendering this message: + * - quoteStrings: boolean that tells if you want strings to be wrapped + * in quotes or not. + */ +Messages.Extended = function(messagePieces, options = {}) +{ + Messages.Simple.call(this, null, options); + + this._messagePieces = messagePieces; + + if ("quoteStrings" in options) { + this._quoteStrings = options.quoteStrings; + } + + this._repeatID.quoteStrings = this._quoteStrings; + this._repeatID.messagePieces = messagePieces + ""; + this._repeatID.actors = new Set(); // using a set to avoid duplicates +}; + +Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype, +{ + /** + * The message pieces displayed by this message instance. + * @private + * @type array + */ + _messagePieces: null, + + /** + * Boolean that tells if the strings displayed in this message are wrapped. + * @private + * @type boolean + */ + _quoteStrings: true, + + getRepeatID: function() + { + if (this._repeatID.uid) { + return JSON.stringify({ uid: this._repeatID.uid }); + } + + // Sets are not stringified correctly. Temporarily switching to an array. + let actors = this._repeatID.actors; + this._repeatID.actors = [...actors]; + let result = JSON.stringify(this._repeatID); + this._repeatID.actors = actors; + return result; + }, + + render: function() + { + let result = this.document.createDocumentFragment(); + + for (let i = 0; i < this._messagePieces.length; i++) { + let separator = i > 0 ? this._renderBodyPieceSeparator() : null; + if (separator) { + result.appendChild(separator); + } + + let piece = this._messagePieces[i]; + result.appendChild(this._renderBodyPiece(piece)); + } + + this._message = result; + this._messagePieces = null; + return Messages.Simple.prototype.render.call(this); + }, + + /** + * Render the separator between the pieces of the message. + * + * @private + * @return Element + */ + _renderBodyPieceSeparator: function() { return null; }, + + /** + * Render one piece/element of the message array. + * + * @private + * @param mixed piece + * Message element to display - this can be a LongString, ObjectActor, + * DOM node or a function to invoke. + * @return Element + */ + _renderBodyPiece: function(piece) + { + if (piece instanceof Ci.nsIDOMNode) { + return piece; + } + if (typeof piece == "function") { + return piece(this); + } + + return this._renderValueGrip(piece); + }, + + /** + * Render a grip that represents a value received from the server. This method + * picks the appropriate widget to render the value with. + * + * @private + * @param object grip + * The value grip received from the server. + * @param object options + * Options for displaying the value. Available options: + * - noStringQuotes - boolean that tells the renderer to not use quotes + * around strings. + * - concise - boolean that tells the renderer to compactly display the + * grip. This is typically set to true when the object needs to be + * displayed in an array preview, or as a property value in object + * previews, etc. + * @return DOMElement + * The DOM element that displays the given grip. + */ + _renderValueGrip: function(grip, options = {}) + { + let isPrimitive = VariablesView.isPrimitive({ value: grip }); + let isActorGrip = WebConsoleUtils.isActorGrip(grip); + let noStringQuotes = !this._quoteStrings; + if ("noStringQuotes" in options) { + noStringQuotes = options.noStringQuotes; + } + + if (isActorGrip) { + this._repeatID.actors.add(grip.actor); + + if (!isPrimitive) { + return this._renderObjectActor(grip, options); + } + if (grip.type == "longString") { + let widget = new Widgets.LongString(this, grip, options).render(); + return widget.element; + } + } + + let result = this.document.createElementNS(XHTML_NS, "span"); + if (isPrimitive) { + if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) { + let widget = new Widgets.URLString(this, grip, options).render(); + return widget.element; + } + + let className = this.getClassNameForValueGrip(grip); + if (className) { + result.className = className; + } + + result.textContent = VariablesView.getString(grip, { + noStringQuotes: noStringQuotes, + concise: options.concise, + }); + } else { + result.textContent = grip; + } + + return result; + }, + + /** + * Shorten grips of the type string, leaves other grips unmodified. + * + * @param object grip + * Value grip from the server. + * @return object + * Possible values of object: + * - A shortened string, if original grip was of string type. + * - The unmodified input grip, if it wasn't of string type. + */ + shortenValueGrip: function(grip) + { + let shortVal = grip; + if (typeof(grip)=="string") { + shortVal = grip.replace(/(\r\n|\n|\r)/gm," "); + if (shortVal.length > MAX_STRING_GRIP_LENGTH) { + shortVal = shortVal.substring(0,MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS; + } + } + + return shortVal; + }, + + /** + * Get a CodeMirror-compatible class name for a given value grip. + * + * @param object grip + * Value grip from the server. + * @return string + * The class name for the grip. + */ + getClassNameForValueGrip: function(grip) + { + let map = { + "number": "cm-number", + "longstring": "console-string", + "string": "console-string", + "regexp": "cm-string-2", + "boolean": "cm-atom", + "-infinity": "cm-atom", + "infinity": "cm-atom", + "null": "cm-atom", + "undefined": "cm-comment", + "symbol": "cm-atom" + }; + + let className = map[typeof grip]; + if (!className && grip && grip.type) { + className = map[grip.type.toLowerCase()]; + } + if (!className && grip && grip.class) { + className = map[grip.class.toLowerCase()]; + } + + return className; + }, + + /** + * Display an object actor with the appropriate renderer. + * + * @private + * @param object objectActor + * The ObjectActor to display. + * @param object options + * Options to use for displaying the ObjectActor. + * @see this._renderValueGrip for the available options. + * @return DOMElement + * The DOM element that displays the object actor. + */ + _renderObjectActor: function(objectActor, options = {}) + { + let widget = Widgets.ObjectRenderers.byClass[objectActor.class]; + + let { preview } = objectActor; + if ((!widget || (widget.canRender && !widget.canRender(objectActor))) + && preview + && preview.kind) { + widget = Widgets.ObjectRenderers.byKind[preview.kind]; + } + + if (!widget || (widget.canRender && !widget.canRender(objectActor))) { + widget = Widgets.JSObject; + } + + let instance = new widget(this, objectActor, options).render(); + return instance.element; + }, +}); // Messages.Extended.prototype + + + +/** + * The JavaScriptEvalOutput message. + * + * @constructor + * @extends Messages.Extended + * @param object evalResponse + * The evaluation response packet received from the server. + * @param string [errorMessage] + * Optional error message to display. + */ +Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage) +{ + let severity = "log", msg, quoteStrings = true; + + // Store also the response packet from the back end. It might + // be useful to extensions customizing the console output. + this.response = evalResponse; + + if (errorMessage) { + severity = "error"; + msg = errorMessage; + quoteStrings = false; + } else { + msg = evalResponse.result; + } + + let options = { + className: "cm-s-mozilla", + timestamp: evalResponse.timestamp, + category: "output", + severity: severity, + quoteStrings: quoteStrings, + }; + Messages.Extended.call(this, [msg], options); +}; + +Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype; + +/** + * The ConsoleGeneric message is used for console API calls. + * + * @constructor + * @extends Messages.Extended + * @param object packet + * The Console API call packet received from the server. + */ +Messages.ConsoleGeneric = function(packet) +{ + let options = { + className: "cm-s-mozilla", + timestamp: packet.timeStamp, + category: "webdev", + severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], + private: packet.private, + filterDuplicates: true, + location: { + url: packet.filename, + line: packet.lineNumber, + column: packet.columnNumber + }, + }; + + switch (packet.level) { + case "count": { + let counter = packet.counter, label = counter.label; + if (!label) { + label = l10n.getStr("noCounterLabel"); + } + Messages.Extended.call(this, [label+ ": " + counter.count], options); + break; + } + default: + Messages.Extended.call(this, packet.arguments, options); + break; + } + + this._repeatID.consoleApiLevel = packet.level; + this._repeatID.styles = packet.styles; + this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; + this._styles = packet.styles || []; + + this._onClickCollapsible = this._onClickCollapsible.bind(this); +}; + +Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype, +{ + _styles: null, + _stacktrace: null, + + /** + * Tells if the message can be expanded/collapsed. + * @type boolean + */ + collapsible: false, + + /** + * Getter that tells if this message is collapsed - no details are shown. + * @type boolean + */ + get collapsed() { + return this.collapsible && this.element && !this.element.hasAttribute("open"); + }, + + _renderBodyPieceSeparator: function() + { + return this.document.createTextNode(" "); + }, + + render: function() + { + let msg = this.document.createElementNS(XHTML_NS, "span"); + msg.className = "message-body devtools-monospace"; + + this._renderBodyPieces(msg); + + let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this); + let location = Messages.Simple.prototype._renderLocation.call(this); + if (location) { + location.target = "jsdebugger"; + } + + let stack = null; + let twisty = null; + if (this._stacktrace && this._stacktrace.length > 0) { + stack = new Widgets.Stacktrace(this, this._stacktrace).render().element; + + twisty = this.document.createElementNS(XHTML_NS, "a"); + twisty.className = "theme-twisty"; + twisty.href = "#"; + twisty.title = l10n.getStr("messageToggleDetails"); + twisty.addEventListener("click", this._onClickCollapsible); + } + + let flex = this.document.createElementNS(XHTML_NS, "span"); + flex.className = "message-flex-body"; + + if (twisty) { + flex.appendChild(twisty); + } + + flex.appendChild(msg); + + if (repeatNode) { + flex.appendChild(repeatNode); + } + if (location) { + flex.appendChild(location); + } + + let result = this.document.createDocumentFragment(); + result.appendChild(flex); + + if (stack) { + result.appendChild(this.document.createTextNode("\n")); + result.appendChild(stack); + } + + this._message = result; + this._stacktrace = null; + + Messages.Simple.prototype.render.call(this); + + if (stack) { + this.collapsible = true; + this.element.setAttribute("collapsible", true); + + let icon = this.element.querySelector(".icon"); + icon.addEventListener("click", this._onClickCollapsible); + } + + return this; + }, + + _renderBody: function() + { + let body = Messages.Simple.prototype._renderBody.apply(this, arguments); + body.classList.remove("devtools-monospace", "message-body"); + return body; + }, + + _renderBodyPieces: function(container) + { + let lastStyle = null; + + for (let i = 0; i < this._messagePieces.length; i++) { + let separator = i > 0 ? this._renderBodyPieceSeparator() : null; + if (separator) { + container.appendChild(separator); + } + + let piece = this._messagePieces[i]; + let style = this._styles[i]; + + // No long string support. + if (style && typeof style == "string" ) { + lastStyle = this.cleanupStyle(style); + } + + container.appendChild(this._renderBodyPiece(piece, lastStyle)); + } + + this._messagePieces = null; + this._styles = null; + }, + + _renderBodyPiece: function(piece, style) + { + let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece); + let result = elem; + + if (style) { + if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { + elem.style = style; + } else { + let span = this.document.createElementNS(XHTML_NS, "span"); + span.style = style; + span.appendChild(elem); + result = span; + } + } + + return result; + }, + + // no-op for the message location and .repeats elements. + // |this.render()| handles customized message output. + _renderLocation: function() { }, + _renderRepeatNode: function() { }, + + /** + * Expand/collapse message details. + */ + toggleDetails: function() + { + let twisty = this.element.querySelector(".theme-twisty"); + if (this.element.hasAttribute("open")) { + this.element.removeAttribute("open"); + twisty.removeAttribute("open"); + } else { + this.element.setAttribute("open", true); + twisty.setAttribute("open", true); + } + }, + + /** + * The click event handler for the message expander arrow element. This method + * toggles the display of message details. + * + * @private + * @param nsIDOMEvent ev + * The DOM event object. + * @see this.toggleDetails() + */ + _onClickCollapsible: function(ev) + { + ev.preventDefault(); + this.toggleDetails(); + }, + + /** + * Given a style attribute value, return a cleaned up version of the string + * such that: + * + * - no external URL is allowed to load. See RE_CLEANUP_STYLES. + * - only some of the properties are allowed, based on a whitelist. See + * RE_ALLOWED_STYLES. + * + * @param string style + * The style string to cleanup. + * @return string + * The style value after cleanup. + */ + cleanupStyle: function(style) + { + for (let r of RE_CLEANUP_STYLES) { + style = style.replace(r, "notallowed"); + } + + let dummy = this.output._dummyElement; + if (!dummy) { + dummy = this.output._dummyElement = + this.document.createElementNS(XHTML_NS, "div"); + } + dummy.style = style; + + let toRemove = []; + for (let i = 0; i < dummy.style.length; i++) { + let prop = dummy.style[i]; + if (!RE_ALLOWED_STYLES.test(prop)) { + toRemove.push(prop); + } + } + + for (let prop of toRemove) { + dummy.style.removeProperty(prop); + } + + style = dummy.style.cssText; + + dummy.style = ""; + + return style; + }, +}); // Messages.ConsoleGeneric.prototype + +/** + * The ConsoleTrace message is used for console.trace() calls. + * + * @constructor + * @extends Messages.Simple + * @param object packet + * The Console API call packet received from the server. + */ +Messages.ConsoleTrace = function(packet) +{ + let options = { + className: "cm-s-mozilla", + timestamp: packet.timeStamp, + category: "webdev", + severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], + private: packet.private, + filterDuplicates: true, + location: { + url: packet.filename, + line: packet.lineNumber, + }, + }; + + this._renderStack = this._renderStack.bind(this); + Messages.Simple.call(this, this._renderStack, options); + + this._repeatID.consoleApiLevel = packet.level; + this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; + this._arguments = packet.arguments; +}; + +Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype, +{ + /** + * Holds the stackframes received from the server. + * + * @private + * @type array + */ + _stacktrace: null, + + /** + * Holds the arguments the content script passed to the console.trace() + * method. This array is cleared when the message is initialized, and + * associated actors are released. + * + * @private + * @type array + */ + _arguments: null, + + init: function() + { + let result = Messages.Simple.prototype.init.apply(this, arguments); + + // We ignore console.trace() arguments. Release object actors. + if (Array.isArray(this._arguments)) { + for (let arg of this._arguments) { + if (WebConsoleUtils.isActorGrip(arg)) { + this.output._releaseObject(arg.actor); + } + } + } + this._arguments = null; + + return result; + }, + + render: function() + { + Messages.Simple.prototype.render.apply(this, arguments); + this.element.setAttribute("open", true); + return this; + }, + + /** + * Render the stack frames. + * + * @private + * @return DOMElement + */ + _renderStack: function() + { + let cmvar = this.document.createElementNS(XHTML_NS, "span"); + cmvar.className = "cm-variable"; + cmvar.textContent = "console"; + + let cmprop = this.document.createElementNS(XHTML_NS, "span"); + cmprop.className = "cm-property"; + cmprop.textContent = "trace"; + + let title = this.document.createElementNS(XHTML_NS, "span"); + title.className = "message-body devtools-monospace"; + title.appendChild(cmvar); + title.appendChild(this.document.createTextNode(".")); + title.appendChild(cmprop); + title.appendChild(this.document.createTextNode("():")); + + let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this); + let location = Messages.Simple.prototype._renderLocation.call(this); + if (location) { + location.target = "jsdebugger"; + } + + let widget = new Widgets.Stacktrace(this, this._stacktrace).render(); + + let body = this.document.createElementNS(XHTML_NS, "span"); + body.className = "message-flex-body"; + body.appendChild(title); + if (repeatNode) { + body.appendChild(repeatNode); + } + if (location) { + body.appendChild(location); + } + body.appendChild(this.document.createTextNode("\n")); + + let frag = this.document.createDocumentFragment(); + frag.appendChild(body); + frag.appendChild(widget.element); + + return frag; + }, + + _renderBody: function() + { + let body = Messages.Simple.prototype._renderBody.apply(this, arguments); + body.classList.remove("devtools-monospace", "message-body"); + return body; + }, + + // no-op for the message location and .repeats elements. + // |this._renderStack| handles customized message output. + _renderLocation: function() { }, + _renderRepeatNode: function() { }, +}); // Messages.ConsoleTrace.prototype + +/** + * The ConsoleTable message is used for console.table() calls. + * + * @constructor + * @extends Messages.Extended + * @param object packet + * The Console API call packet received from the server. + */ +Messages.ConsoleTable = function(packet) +{ + let options = { + className: "cm-s-mozilla", + timestamp: packet.timeStamp, + category: "webdev", + severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], + private: packet.private, + filterDuplicates: false, + location: { + url: packet.filename, + line: packet.lineNumber, + }, + }; + + this._populateTableData = this._populateTableData.bind(this); + this._renderTable = this._renderTable.bind(this); + Messages.Extended.call(this, [this._renderTable], options); + + this._repeatID.consoleApiLevel = packet.level; + this._arguments = packet.arguments; +}; + +Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype, +{ + /** + * Holds the arguments the content script passed to the console.table() + * method. + * + * @private + * @type array + */ + _arguments: null, + + /** + * Array of objects that holds the data to log in the table. + * + * @private + * @type array + */ + _data: null, + + /** + * Key value pair of the id and display name for the columns in the table. + * Refer to the TableWidget API. + * + * @private + * @type object + */ + _columns: null, + + /** + * A promise that resolves when the table data is ready or null if invalid + * arguments are provided. + * + * @private + * @type promise|null + */ + _populatePromise: null, + + init: function() + { + let result = Messages.Extended.prototype.init.apply(this, arguments); + this._data = []; + this._columns = {}; + + this._populatePromise = this._populateTableData(); + + return result; + }, + + /** + * Sets the key value pair of the id and display name for the columns in the + * table. + * + * @private + * @param array|string columns + * Either a string or array containing the names for the columns in + * the output table. + */ + _setColumns: function(columns) + { + if (columns.class == "Array") { + let items = columns.preview.items; + + for (let item of items) { + if (typeof item == "string") { + this._columns[item] = item; + } + } + } else if (typeof columns == "string" && columns) { + this._columns[columns] = columns; + } + }, + + /** + * Retrieves the table data and columns from the arguments received from the + * server. + * + * @return Promise|null + * Returns a promise that resolves when the table data is ready or + * null if the arguments are invalid. + */ + _populateTableData: function() + { + let deferred = promise.defer(); + + if (this._arguments.length <= 0) { + return; + } + + let data = this._arguments[0]; + if (data.class != "Array" && data.class != "Object" && + data.class != "Map" && data.class != "Set") { + return; + } + + let hasColumnsArg = false; + if (this._arguments.length > 1) { + if (data.class == "Object" || data.class == "Array") { + this._columns["_index"] = l10n.getStr("table.index"); + } else { + this._columns["_index"] = l10n.getStr("table.iterationIndex"); + } + + this._setColumns(this._arguments[1]); + hasColumnsArg = true; + } + + if (data.class == "Object" || data.class == "Array") { + // Get the object properties, and parse the key and value properties into + // the table data and columns. + this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client, + data); + this.client.getPrototypeAndProperties(aResponse => { + let {ownProperties} = aResponse; + let rowCount = 0; + let columnCount = 0; + + for (let index of Object.keys(ownProperties || {})) { + // Avoid outputting the length property if the data argument provided + // is an array + if (data.class == "Array" && index == "length") { + continue; + } + + if (!hasColumnsArg) { + this._columns["_index"] = l10n.getStr("table.index"); + } + + if (data.class == "Array") { + if (index == parseInt(index)) { + index = parseInt(index); + } + } + + let property = ownProperties[index].value; + let item = { _index: index }; + + if (property.class == "Object" || property.class == "Array") { + let {preview} = property; + let entries = property.class == "Object" ? + preview.ownProperties : preview.items; + + for (let key of Object.keys(entries)) { + let value = property.class == "Object" ? + preview.ownProperties[key].value : preview.items[key]; + + item[key] = this._renderValueGrip(value, { concise: true }); + + if (!hasColumnsArg && !(key in this._columns) && + (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) { + this._columns[key] = key; + } + } + } else { + // Display the value for any non-object data input. + item["_value"] = this._renderValueGrip(property, { concise: true }); + + if (!hasColumnsArg && !("_value" in this._columns)) { + this._columns["_value"] = l10n.getStr("table.value"); + } + } + + this._data.push(item); + + if (++rowCount == TABLE_ROW_MAX_ITEMS) { + break; + } + } + + deferred.resolve(); + }); + } else if (data.class == "Map") { + let entries = data.preview.entries; + + if (!hasColumnsArg) { + this._columns["_index"] = l10n.getStr("table.iterationIndex"); + this._columns["_key"] = l10n.getStr("table.key"); + this._columns["_value"] = l10n.getStr("table.value"); + } + + let rowCount = 0; + for (let [key, value] of entries) { + let item = { + _index: rowCount, + _key: this._renderValueGrip(key, { concise: true }), + _value: this._renderValueGrip(value, { concise: true }) + }; + + this._data.push(item); + + if (++rowCount == TABLE_ROW_MAX_ITEMS) { + break; + } + } + + deferred.resolve(); + } else if (data.class == "Set") { + let entries = data.preview.items; + + if (!hasColumnsArg) { + this._columns["_index"] = l10n.getStr("table.iterationIndex"); + this._columns["_value"] = l10n.getStr("table.value"); + } + + let rowCount = 0; + for (let entry of entries) { + let item = { + _index : rowCount, + _value: this._renderValueGrip(entry, { concise: true }) + }; + + this._data.push(item); + + if (++rowCount == TABLE_ROW_MAX_ITEMS) { + break; + } + } + + deferred.resolve(); + } + + return deferred.promise; + }, + + render: function() + { + Messages.Extended.prototype.render.apply(this, arguments); + this.element.setAttribute("open", true); + return this; + }, + + /** + * Render the table. + * + * @private + * @return DOMElement + */ + _renderTable: function() + { + let cmvar = this.document.createElementNS(XHTML_NS, "span"); + cmvar.className = "cm-variable"; + cmvar.textContent = "console"; + + let cmprop = this.document.createElementNS(XHTML_NS, "span"); + cmprop.className = "cm-property"; + cmprop.textContent = "table"; + + let title = this.document.createElementNS(XHTML_NS, "span"); + title.className = "message-body devtools-monospace"; + title.appendChild(cmvar); + title.appendChild(this.document.createTextNode(".")); + title.appendChild(cmprop); + title.appendChild(this.document.createTextNode("():")); + + let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this); + let location = Messages.Simple.prototype._renderLocation.call(this); + if (location) { + location.target = "jsdebugger"; + } + + let body = this.document.createElementNS(XHTML_NS, "span"); + body.className = "message-flex-body"; + body.appendChild(title); + if (repeatNode) { + body.appendChild(repeatNode); + } + if (location) { + body.appendChild(location); + } + body.appendChild(this.document.createTextNode("\n")); + + let result = this.document.createElementNS(XHTML_NS, "div"); + result.appendChild(body); + + if (this._populatePromise) { + this._populatePromise.then(() => { + if (this._data.length > 0) { + let widget = new Widgets.Table(this, this._data, this._columns).render(); + result.appendChild(widget.element); + } + + result.scrollIntoView(); + this.output.owner.emit("messages-table-rendered"); + + // Release object actors + if (Array.isArray(this._arguments)) { + for (let arg of this._arguments) { + if (WebConsoleUtils.isActorGrip(arg)) { + this.output._releaseObject(arg.actor); + } + } + } + this._arguments = null; + }); + } + + return result; + }, + + _renderBody: function() + { + let body = Messages.Simple.prototype._renderBody.apply(this, arguments); + body.classList.remove("devtools-monospace", "message-body"); + return body; + }, + + // no-op for the message location and .repeats elements. + // |this._renderTable| handles customized message output. + _renderLocation: function() { }, + _renderRepeatNode: function() { }, +}); // Messages.ConsoleTable.prototype + +let Widgets = {}; + +/** + * The base widget class. + * + * @constructor + * @param object message + * The owning message. + */ +Widgets.BaseWidget = function(message) +{ + this.message = message; +}; + +Widgets.BaseWidget.prototype = { + /** + * The owning message object. + * @type object + */ + message: null, + + /** + * The DOM element of the rendered widget. + * @type Element + */ + element: null, + + /** + * Getter for the DOM document that holds the output. + * @type Document + */ + get document() { + return this.message.document; + }, + + /** + * The ConsoleOutput instance that owns this widget instance. + */ + get output() { + return this.message.output; + }, + + /** + * Render the widget DOM element. + * @return this + */ + render: function() { }, + + /** + * Destroy this widget instance. + */ + destroy: function() { }, + + /** + * Helper for creating DOM elements for widgets. + * + * Usage: + * this.el("tag#id.class.names"); // create element "tag" with ID "id" and + * two class names, .class and .names. + * + * this.el("span", { attr1: "value1", ... }) // second argument can be an + * object that holds element attributes and values for the new DOM element. + * + * this.el("p", { attr1: "value1", ... }, "text content"); // the third + * argument can include the default .textContent of the new DOM element. + * + * this.el("p", "text content"); // if the second argument is not an object, + * it will be used as .textContent for the new DOM element. + * + * @param string tagNameIdAndClasses + * Tag name for the new element, optionally followed by an ID and/or + * class names. Examples: "span", "div#fooId", "div.class.names", + * "p#id.class". + * @param string|object [attributesOrTextContent] + * If this argument is an object it will be used to set the attributes + * of the new DOM element. Otherwise, the value becomes the + * .textContent of the new DOM element. + * @param string [textContent] + * If this argument is provided the value is used as the textContent of + * the new DOM element. + * @return DOMElement + * The new DOM element. + */ + el: function(tagNameIdAndClasses) + { + let attrs, text; + if (typeof arguments[1] == "object") { + attrs = arguments[1]; + text = arguments[2]; + } else { + text = arguments[1]; + } + + let tagName = tagNameIdAndClasses.split(/#|\./)[0]; + + let elem = this.document.createElementNS(XHTML_NS, tagName); + for (let name of Object.keys(attrs || {})) { + elem.setAttribute(name, attrs[name]); + } + if (text !== undefined && text !== null) { + elem.textContent = text; + } + + let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g); + for (let idOrClass of (idAndClasses || [])) { + if (idOrClass.charAt(0) == "#") { + elem.id = idOrClass.substr(1); + } else { + elem.classList.add(idOrClass.substr(1)); + } + } + + return elem; + }, +}; + +/** + * The timestamp widget. + * + * @constructor + * @param object message + * The owning message. + * @param number timestamp + * The UNIX timestamp to display. + */ +Widgets.MessageTimestamp = function(message, timestamp) +{ + Widgets.BaseWidget.call(this, message); + this.timestamp = timestamp; +}; + +Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * The UNIX timestamp. + * @type number + */ + timestamp: 0, + + render: function() + { + if (this.element) { + return this; + } + + this.element = this.document.createElementNS(XHTML_NS, "span"); + this.element.className = "timestamp devtools-monospace"; + this.element.textContent = l10n.timestampString(this.timestamp) + " "; + + return this; + }, +}); // Widgets.MessageTimestamp.prototype + + +/** + * The URLString widget, for rendering strings where at least one token is a + * URL. + * + * @constructor + * @param object message + * The owning message. + * @param string str + * The string, which contains at least one valid URL. + */ +Widgets.URLString = function(message, str) +{ + Widgets.BaseWidget.call(this, message); + this.str = str; +}; + +Widgets.URLString.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * The string to format, which contains at least one valid URL. + * @type string + */ + str: "", + + render: function() + { + if (this.element) { + return this; + } + + // The rendered URLString will be a <span> containing a number of text + // <spans> for non-URL tokens and <a>'s for URL tokens. + this.element = this.el("span", { + class: "console-string" + }); + this.element.appendChild(this._renderText("\"")); + + // As we walk through the tokens of the source string, we make sure to preserve + // the original whitespace that seperated the tokens. + let tokens = this.str.split(/\s+/); + let textStart = 0; + let tokenStart; + for (let token of tokens) { + tokenStart = this.str.indexOf(token, textStart); + if (this._isURL(token)) { + this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart))); + textStart = tokenStart + token.length; + this.element.appendChild(this._renderURL(token)); + } + } + + // Clean up any non-URL text at the end of the source string. + this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length))); + this.element.appendChild(this._renderText("\"")); + + return this; + }, + + /** + * Determines whether a grip is a string containing a URL. + * + * @param string grip + * The grip, which may contain a URL. + * @return boolean + * Whether the grip is a string containing a URL. + */ + containsURL: function(grip) + { + if (typeof grip != "string") { + return false; + } + + let tokens = grip.split(/\s+/); + return tokens.some(this._isURL); + }, + + /** + * Determines whether a string token is a valid URL. + * + * @param string token + * The token. + * @return boolean + * Whenther the token is a URL. + */ + _isURL: function(token) { + try { + let uri = URI.newURI(token, null, null); + let url = uri.QueryInterface(Ci.nsIURL); + return true; + } catch (e) { + return false; + } + }, + + /** + * Renders a string as a URL. + * + * @param string url + * The string to be rendered as a url. + * @return DOMElement + * An element containing the rendered string. + */ + _renderURL: function(url) + { + let result = this.el("a", { + class: "url", + title: url, + href: url, + draggable: false + }, url); + this.message._addLinkCallback(result); + return result; + }, + + _renderText: function(text) { + return this.el("span", text); + }, +}); // Widgets.URLString.prototype + +/** + * Widget used for displaying ObjectActors that have no specialised renderers. + * + * @constructor + * @param object message + * The owning message. + * @param object objectActor + * The ObjectActor to display. + * @param object [options] + * Options for displaying the given ObjectActor. See + * Messages.Extended.prototype._renderValueGrip for the available + * options. + */ +Widgets.JSObject = function(message, objectActor, options = {}) +{ + Widgets.BaseWidget.call(this, message); + this.objectActor = objectActor; + this.options = options; + this._onClick = this._onClick.bind(this); +}; + +Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * The ObjectActor displayed by the widget. + * @type object + */ + objectActor: null, + + render: function() + { + if (!this.element) { + this._render(); + } + + return this; + }, + + _render: function() + { + let str = VariablesView.getString(this.objectActor, this.options); + let className = this.message.getClassNameForValueGrip(this.objectActor); + if (!className && this.objectActor.class == "Object") { + className = "cm-variable"; + } + + this.element = this._anchor(str, { className: className }); + }, + + /** + * Render a concise representation of an object. + */ + _renderConciseObject: function() + { + this.element = this._anchor(this.objectActor.class, + { className: "cm-variable" }); + }, + + /** + * Render the `<class> { ` prefix of an object. + */ + _renderObjectPrefix: function() + { + let { kind } = this.objectActor.preview; + this.element = this.el("span.kind-" + kind); + this._anchor(this.objectActor.class, { className: "cm-variable" }); + this._text(" { "); + }, + + /** + * Render the ` }` suffix of an object. + */ + _renderObjectSuffix: function() + { + this._text(" }"); + }, + + /** + * Render an object property. + * + * @param String key + * The property name. + * @param Object value + * The property value, as an RDP grip. + * @param nsIDOMNode container + * The container node to render to. + * @param Boolean needsComma + * True if there was another property before this one and we need to + * separate them with a comma. + * @param Boolean valueIsText + * Add the value as is, don't treat it as a grip and pass it to + * `_renderValueGrip`. + */ + _renderObjectProperty: function(key, value, container, needsComma, valueIsText = false) + { + if (needsComma) { + this._text(", "); + } + + container.appendChild(this.el("span.cm-property", key)); + this._text(": "); + + if (valueIsText) { + this._text(value); + } else { + let shortVal = this.message.shortenValueGrip(value); + let valueElem = this.message._renderValueGrip(shortVal, { concise: true }); + container.appendChild(valueElem); + } + }, + + /** + * Render this object's properties. + * + * @param nsIDOMNode container + * The container node to render to. + * @param Boolean needsComma + * True if there was another property before this one and we need to + * separate them with a comma. + */ + _renderObjectProperties: function(container, needsComma) + { + let { preview } = this.objectActor; + let { ownProperties, safeGetterValues } = preview; + + let shown = 0; + + let getValue = desc => { + if (desc.get) { + return "Getter"; + } else if (desc.set) { + return "Setter"; + } else { + return desc.value; + } + }; + + for (let key of Object.keys(ownProperties || {})) { + this._renderObjectProperty(key, getValue(ownProperties[key]), container, + shown > 0 || needsComma, + ownProperties[key].get || ownProperties[key].set); + shown++; + } + + let ownPropertiesShown = shown; + + for (let key of Object.keys(safeGetterValues || {})) { + this._renderObjectProperty(key, safeGetterValues[key].getterValue, + container, shown > 0 || needsComma); + shown++; + } + + if (typeof preview.ownPropertiesLength == "number" && + ownPropertiesShown < preview.ownPropertiesLength) { + this._text(", "); + + let n = preview.ownPropertiesLength - ownPropertiesShown; + let str = VariablesView.stringifiers._getNMoreString(n); + this._anchor(str); + } + }, + + /** + * Render an anchor with a given text content and link. + * + * @private + * @param string text + * Text to show in the anchor. + * @param object [options] + * Available options: + * - onClick (function): "click" event handler.By default a click on + * the anchor opens the variables view for the current object actor + * (this.objectActor). + * - href (string): if given the string is used as a link, and clicks + * on the anchor open the link in a new tab. + * - appendTo (DOMElement): append the element to the given DOM + * element. If not provided, the anchor is appended to |this.element| + * if it is available. If |appendTo| is provided and if it is a falsy + * value, the anchor is not appended to any element. + * @return DOMElement + * The DOM element of the new anchor. + */ + _anchor: function(text, options = {}) + { + if (!options.onClick) { + // If the anchor has an URL, open it in a new tab. If not, show the + // current object actor. + options.onClick = options.href ? this._onClickAnchor : this._onClick; + } + + let anchor = this.el("a", { + class: options.className, + draggable: false, + href: options.href || "#", + }, text); + + this.message._addLinkCallback(anchor, options.onClick); + + if (options.appendTo) { + options.appendTo.appendChild(anchor); + } else if (!("appendTo" in options) && this.element) { + this.element.appendChild(anchor); + } + + return anchor; + }, + + /** + * The click event handler for objects shown inline. + * @private + */ + _onClick: function() + { + this.output.openVariablesView({ + label: VariablesView.getString(this.objectActor, { concise: true }), + objectActor: this.objectActor, + autofocus: true, + }); + }, + + /** + * Add a string to the message. + * + * @private + * @param string str + * String to add. + * @param DOMElement [target = this.element] + * Optional DOM element to append the string to. The default is + * this.element. + */ + _text: function(str, target = this.element) + { + target.appendChild(this.document.createTextNode(str)); + }, +}); // Widgets.JSObject.prototype + +Widgets.ObjectRenderers = {}; +Widgets.ObjectRenderers.byKind = {}; +Widgets.ObjectRenderers.byClass = {}; + +/** + * Add an object renderer. + * + * @param object obj + * An object that represents the renderer. Properties: + * - byClass (string, optional): this renderer will be used for the given + * object class. + * - byKind (string, optional): this renderer will be used for the given + * object kind. + * One of byClass or byKind must be provided. + * - extends (object, optional): the renderer object extends the given + * object. Default: Widgets.JSObject. + * - canRender (function, optional): this method is invoked when + * a candidate object needs to be displayed. The method is invoked as + * a static method, as such, none of the properties of the renderer + * object will be available. You get one argument: the object actor grip + * received from the server. If the method returns true, then this + * renderer is used for displaying the object, otherwise not. + * - initialize (function, optional): the constructor of the renderer + * widget. This function is invoked with the following arguments: the + * owner message object instance, the object actor grip to display, and + * an options object. See Messages.Extended.prototype._renderValueGrip() + * for details about the options object. + * - render (function, required): the method that displays the given + * object actor. + */ +Widgets.ObjectRenderers.add = function(obj) +{ + let extendObj = obj.extends || Widgets.JSObject; + + let constructor = function() { + if (obj.initialize) { + obj.initialize.apply(this, arguments); + } else { + extendObj.apply(this, arguments); + } + }; + + let proto = WebConsoleUtils.cloneObject(obj, false, function(key) { + if (key == "initialize" || key == "canRender" || + (key == "render" && extendObj === Widgets.JSObject)) { + return false; + } + return true; + }); + + if (extendObj === Widgets.JSObject) { + proto._render = obj.render; + } + + constructor.canRender = obj.canRender; + constructor.prototype = Heritage.extend(extendObj.prototype, proto); + + if (obj.byClass) { + Widgets.ObjectRenderers.byClass[obj.byClass] = constructor; + } else if (obj.byKind) { + Widgets.ObjectRenderers.byKind[obj.byKind] = constructor; + } else { + throw new Error("You are adding an object renderer without any byClass or " + + "byKind property."); + } +}; + + +/** + * The widget used for displaying Date objects. + */ +Widgets.ObjectRenderers.add({ + byClass: "Date", + + render: function() + { + let {preview} = this.objectActor; + this.element = this.el("span.class-" + this.objectActor.class); + + let anchorText = this.objectActor.class; + let anchorClass = "cm-variable"; + if (preview && "timestamp" in preview && typeof preview.timestamp != "number") { + anchorText = new Date(preview.timestamp).toString(); // invalid date + anchorClass = ""; + } + + this._anchor(anchorText, { className: anchorClass }); + + if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") { + return; + } + + this._text(" "); + + let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString()); + this.element.appendChild(elem); + }, +}); + +/** + * The widget used for displaying Function objects. + */ +Widgets.ObjectRenderers.add({ + byClass: "Function", + + render: function() + { + let grip = this.objectActor; + this.element = this.el("span.class-" + this.objectActor.class); + + // TODO: Bug 948484 - support arrow functions and ES6 generators + let name = grip.userDisplayName || grip.displayName || grip.name || ""; + name = VariablesView.getString(name, { noStringQuotes: true }); + + let str = this.options.concise ? name || "function " : "function " + name; + + if (this.options.concise) { + this._anchor(name || "function", { + className: name ? "cm-variable" : "cm-keyword", + }); + if (!name) { + this._text(" "); + } + } else if (name) { + this.element.appendChild(this.el("span.cm-keyword", "function")); + this._text(" "); + this._anchor(name, { className: "cm-variable" }); + } else { + this._anchor("function", { className: "cm-keyword" }); + this._text(" "); + } + + this._text("("); + + // TODO: Bug 948489 - Support functions with destructured parameters and + // rest parameters + let params = grip.parameterNames || []; + let shown = 0; + for (let param of params) { + if (shown > 0) { + this._text(", "); + } + this.element.appendChild(this.el("span.cm-def", param)); + shown++; + } + + this._text(")"); + }, +}); // Widgets.ObjectRenderers.byClass.Function + +/** + * The widget used for displaying ArrayLike objects. + */ +Widgets.ObjectRenderers.add({ + byKind: "ArrayLike", + + render: function() + { + let {preview} = this.objectActor; + let {items} = preview; + this.element = this.el("span.kind-" + preview.kind); + + this._anchor(this.objectActor.class, { className: "cm-variable" }); + + if (!items || this.options.concise) { + this._text("["); + this.element.appendChild(this.el("span.cm-number", preview.length)); + this._text("]"); + return this; + } + + this._text(" [ "); + + let isFirst = true; + let emptySlots = 0; + // A helper that renders a comma between items if isFirst == false. + let renderSeparator = () => !isFirst && this._text(", "); + + for (let item of items) { + if (item === null) { + emptySlots++; + } + else { + renderSeparator(); + isFirst = false; + + if (emptySlots) { + this._renderEmptySlots(emptySlots); + emptySlots = 0; + } + + let shortVal = this.message.shortenValueGrip(item); + let elem = this.message._renderValueGrip(shortVal, { concise: true }); + this.element.appendChild(elem); + } + } + + if (emptySlots) { + renderSeparator(); + this._renderEmptySlots(emptySlots, false); + } + + let shown = items.length; + if (shown < preview.length) { + this._text(", "); + + let n = preview.length - shown; + let str = VariablesView.stringifiers._getNMoreString(n); + this._anchor(str); + } + + this._text(" ]"); + }, + + _renderEmptySlots: function(aNumSlots, aAppendComma=true) { + let slotLabel = l10n.getStr("emptySlotLabel"); + let slotText = PluralForm.get(aNumSlots, slotLabel); + this._text("<" + slotText.replace("#1", aNumSlots) + ">"); + if (aAppendComma) { + this._text(", "); + } + }, + +}); // Widgets.ObjectRenderers.byKind.ArrayLike + +/** + * The widget used for displaying MapLike objects. + */ +Widgets.ObjectRenderers.add({ + byKind: "MapLike", + + render: function() + { + let {preview} = this.objectActor; + let {entries} = preview; + + let container = this.element = this.el("span.kind-" + preview.kind); + this._anchor(this.objectActor.class, { className: "cm-variable" }); + + if (!entries || this.options.concise) { + if (typeof preview.size == "number") { + this._text("["); + container.appendChild(this.el("span.cm-number", preview.size)); + this._text("]"); + } + return; + } + + this._text(" { "); + + let shown = 0; + for (let [key, value] of entries) { + if (shown > 0) { + this._text(", "); + } + + let keyElem = this.message._renderValueGrip(key, { + concise: true, + noStringQuotes: true, + }); + + // Strings are property names. + if (keyElem.classList && keyElem.classList.contains("console-string")) { + keyElem.classList.remove("console-string"); + keyElem.classList.add("cm-property"); + } + + container.appendChild(keyElem); + + this._text(": "); + + let valueElem = this.message._renderValueGrip(value, { concise: true }); + container.appendChild(valueElem); + + shown++; + } + + if (typeof preview.size == "number" && shown < preview.size) { + this._text(", "); + + let n = preview.size - shown; + let str = VariablesView.stringifiers._getNMoreString(n); + this._anchor(str); + } + + this._text(" }"); + }, +}); // Widgets.ObjectRenderers.byKind.MapLike + +/** + * The widget used for displaying objects with a URL. + */ +Widgets.ObjectRenderers.add({ + byKind: "ObjectWithURL", + + render: function() + { + this.element = this._renderElement(this.objectActor, + this.objectActor.preview.url); + }, + + _renderElement: function(objectActor, url) + { + let container = this.el("span.kind-" + objectActor.preview.kind); + + this._anchor(objectActor.class, { + className: "cm-variable", + appendTo: container, + }); + + if (!VariablesView.isFalsy({ value: url })) { + this._text(" \u2192 ", container); + let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, { + onlyCropQuery: !this.options.concise + }); + this._anchor(shortUrl, { href: url, appendTo: container }); + } + + return container; + }, +}); // Widgets.ObjectRenderers.byKind.ObjectWithURL + +/** + * The widget used for displaying objects with a string next to them. + */ +Widgets.ObjectRenderers.add({ + byKind: "ObjectWithText", + + render: function() + { + let {preview} = this.objectActor; + this.element = this.el("span.kind-" + preview.kind); + + this._anchor(this.objectActor.class, { className: "cm-variable" }); + + if (!this.options.concise) { + this._text(" "); + this.element.appendChild(this.el("span.console-string", + VariablesView.getString(preview.text))); + } + }, +}); + +/** + * The widget used for displaying DOM event previews. + */ +Widgets.ObjectRenderers.add({ + byKind: "DOMEvent", + + render: function() + { + let {preview} = this.objectActor; + + let container = this.element = this.el("span.kind-" + preview.kind); + + this._anchor(preview.type || this.objectActor.class, + { className: "cm-variable" }); + + if (this.options.concise) { + return; + } + + if (preview.eventKind == "key" && preview.modifiers && + preview.modifiers.length) { + this._text(" "); + + let mods = 0; + for (let mod of preview.modifiers) { + if (mods > 0) { + this._text("-"); + } + container.appendChild(this.el("span.cm-keyword", mod)); + mods++; + } + } + + this._text(" { "); + + let shown = 0; + if (preview.target) { + container.appendChild(this.el("span.cm-property", "target")); + this._text(": "); + let target = this.message._renderValueGrip(preview.target, { concise: true }); + container.appendChild(target); + shown++; + } + + for (let key of Object.keys(preview.properties || {})) { + if (shown > 0) { + this._text(", "); + } + + container.appendChild(this.el("span.cm-property", key)); + this._text(": "); + + let value = preview.properties[key]; + let valueElem = this.message._renderValueGrip(value, { concise: true }); + container.appendChild(valueElem); + + shown++; + } + + this._text(" }"); + }, +}); // Widgets.ObjectRenderers.byKind.DOMEvent + +/** + * The widget used for displaying DOM node previews. + */ +Widgets.ObjectRenderers.add({ + byKind: "DOMNode", + + canRender: function(objectActor) { + let {preview} = objectActor; + if (!preview) { + return false; + } + + switch (preview.nodeType) { + case Ci.nsIDOMNode.DOCUMENT_NODE: + case Ci.nsIDOMNode.ATTRIBUTE_NODE: + case Ci.nsIDOMNode.TEXT_NODE: + case Ci.nsIDOMNode.COMMENT_NODE: + case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: + case Ci.nsIDOMNode.ELEMENT_NODE: + return true; + default: + return false; + } + }, + + render: function() + { + switch (this.objectActor.preview.nodeType) { + case Ci.nsIDOMNode.DOCUMENT_NODE: + this._renderDocumentNode(); + break; + case Ci.nsIDOMNode.ATTRIBUTE_NODE: { + let {preview} = this.objectActor; + this.element = this.el("span.attributeNode.kind-" + preview.kind); + let attr = this._renderAttributeNode(preview.nodeName, preview.value, true); + this.element.appendChild(attr); + break; + } + case Ci.nsIDOMNode.TEXT_NODE: + this._renderTextNode(); + break; + case Ci.nsIDOMNode.COMMENT_NODE: + this._renderCommentNode(); + break; + case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: + this._renderDocumentFragmentNode(); + break; + case Ci.nsIDOMNode.ELEMENT_NODE: + this._renderElementNode(); + break; + default: + throw new Error("Unsupported nodeType: " + preview.nodeType); + } + }, + + _renderDocumentNode: function() + { + let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement; + this.element = fn.call(this, this.objectActor, + this.objectActor.preview.location); + this.element.classList.add("documentNode"); + }, + + _renderAttributeNode: function(nodeName, nodeValue, addLink) + { + let value = VariablesView.getString(nodeValue, { noStringQuotes: true }); + + let fragment = this.document.createDocumentFragment(); + if (addLink) { + this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment }); + } else { + fragment.appendChild(this.el("span.cm-attribute", nodeName)); + } + + this._text("=", fragment); + fragment.appendChild(this.el("span.console-string", + '"' + escapeHTML(value) + '"')); + + return fragment; + }, + + _renderTextNode: function() + { + let {preview} = this.objectActor; + this.element = this.el("span.textNode.kind-" + preview.kind); + + this._anchor(preview.nodeName, { className: "cm-variable" }); + this._text(" "); + + let text = VariablesView.getString(preview.textContent); + this.element.appendChild(this.el("span.console-string", text)); + }, + + _renderCommentNode: function() + { + let {preview} = this.objectActor; + let comment = "<!-- " + VariablesView.getString(preview.textContent, { + noStringQuotes: true, + }) + " -->"; + + this.element = this._anchor(comment, { + className: "kind-" + preview.kind + " commentNode cm-comment", + }); + }, + + _renderDocumentFragmentNode: function() + { + let {preview} = this.objectActor; + let {childNodes} = preview; + let container = this.element = this.el("span.documentFragmentNode.kind-" + + preview.kind); + + this._anchor(this.objectActor.class, { className: "cm-variable" }); + + if (!childNodes || this.options.concise) { + this._text("["); + container.appendChild(this.el("span.cm-number", preview.childNodesLength)); + this._text("]"); + return; + } + + this._text(" [ "); + + let shown = 0; + for (let item of childNodes) { + if (shown > 0) { + this._text(", "); + } + + let elem = this.message._renderValueGrip(item, { concise: true }); + container.appendChild(elem); + shown++; + } + + if (shown < preview.childNodesLength) { + this._text(", "); + + let n = preview.childNodesLength - shown; + let str = VariablesView.stringifiers._getNMoreString(n); + this._anchor(str); + } + + this._text(" ]"); + }, + + _renderElementNode: function() + { + let doc = this.document; + let {attributes, nodeName} = this.objectActor.preview; + + this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode"); + + let openTag = this.el("span.cm-tag"); + openTag.textContent = "<"; + this.element.appendChild(openTag); + + let tagName = this._anchor(nodeName, { + className: "cm-tag", + appendTo: openTag + }); + + if (this.options.concise) { + if (attributes.id) { + tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id)); + } + if (attributes.class) { + tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join("."))); + } + } else { + for (let name of Object.keys(attributes)) { + let attr = this._renderAttributeNode(" " + name, attributes[name]); + this.element.appendChild(attr); + } + } + + let closeTag = this.el("span.cm-tag"); + closeTag.textContent = ">"; + this.element.appendChild(closeTag); + + // Register this widget in the owner message so that it gets destroyed when + // the message is destroyed. + this.message.widgets.add(this); + + this.linkToInspector().then(null, Cu.reportError); + }, + + /** + * If the DOMNode being rendered can be highlit in the page, this function + * will attach mouseover/out event listeners to do so, and the inspector icon + * to open the node in the inspector. + * @return a promise that resolves when the node has been linked to the + * inspector, or rejects if it wasn't (either if no toolbox could be found to + * access the inspector, or if the node isn't present in the inspector, i.e. + * if the node is in a DocumentFragment or not part of the tree, or not of + * type Ci.nsIDOMNode.ELEMENT_NODE). + */ + linkToInspector: Task.async(function*() + { + if (this._linkedToInspector) { + return; + } + + // Checking the node type + if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { + throw new Error("The object cannot be linked to the inspector as it " + + "isn't an element node"); + } + + // Checking the presence of a toolbox + let target = this.message.output.toolboxTarget; + this.toolbox = gDevTools.getToolbox(target); + if (!this.toolbox) { + throw new Error("The object cannot be linked to the inspector without a " + + "toolbox"); + } + + // Checking that the inspector supports the node + yield this.toolbox.initInspector(); + this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor); + if (!this._nodeFront) { + throw new Error("The object cannot be linked to the inspector, the " + + "corresponding nodeFront could not be found"); + } + + // At this stage, the message may have been cleared already + if (!this.document) { + throw new Error("The object cannot be linked to the inspector, the " + + "message was got cleared away"); + } + + this.highlightDomNode = this.highlightDomNode.bind(this); + this.element.addEventListener("mouseover", this.highlightDomNode, false); + this.unhighlightDomNode = this.unhighlightDomNode.bind(this); + this.element.addEventListener("mouseout", this.unhighlightDomNode, false); + + this._openInspectorNode = this._anchor("", { + className: "open-inspector", + onClick: this.openNodeInInspector.bind(this) + }); + this._openInspectorNode.title = l10n.getStr("openNodeInInspector"); + + this._linkedToInspector = true; + }), + + /** + * Highlight the DOMNode corresponding to the ObjectActor in the page. + * @return a promise that resolves when the node has been highlighted, or + * rejects if the node cannot be highlighted (detached from the DOM) + */ + highlightDomNode: Task.async(function*() + { + yield this.linkToInspector(); + let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); + if (isAttached) { + yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); + } else { + throw null; + } + }), + + /** + * Unhighlight a previously highlit node + * @see highlightDomNode + * @return a promise that resolves when the highlighter has been hidden + */ + unhighlightDomNode: function() + { + return this.linkToInspector().then(() => { + return this.toolbox.highlighterUtils.unhighlight(); + }).then(null, Cu.reportError); + }, + + /** + * Open the DOMNode corresponding to the ObjectActor in the inspector panel + * @return a promise that resolves when the inspector has been switched to + * and the node has been selected, or rejects if the node cannot be selected + * (detached from the DOM). Note that in any case, the inspector panel will + * be switched to. + */ + openNodeInInspector: Task.async(function*() + { + yield this.linkToInspector(); + yield this.toolbox.selectTool("inspector"); + + let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); + if (isAttached) { + let onReady = this.toolbox.inspector.once("inspector-updated"); + yield this.toolbox.selection.setNodeFront(this._nodeFront, "console"); + yield onReady; + } else { + throw null; + } + }), + + destroy: function() + { + if (this.toolbox && this._nodeFront) { + this.element.removeEventListener("mouseover", this.highlightDomNode, false); + this.element.removeEventListener("mouseout", this.unhighlightDomNode, false); + this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true); + this.toolbox = null; + this._nodeFront = null; + } + }, +}); // Widgets.ObjectRenderers.byKind.DOMNode + +/** + * The widget user for displaying Promise objects. + */ +Widgets.ObjectRenderers.add({ + byClass: "Promise", + + render: function() + { + let { ownProperties, safeGetterValues } = this.objectActor.preview; + if ((!ownProperties && !safeGetterValues) || this.options.concise) { + this._renderConciseObject(); + return; + } + + this._renderObjectPrefix(); + let container = this.element; + let addedPromiseInternalProps = false; + + if (this.objectActor.promiseState) { + const { state, value, reason } = this.objectActor.promiseState; + + this._renderObjectProperty("<state>", state, container, false); + addedPromiseInternalProps = true; + + if (state == "fulfilled") { + this._renderObjectProperty("<value>", value, container, true); + } else if (state == "rejected") { + this._renderObjectProperty("<reason>", reason, container, true); + } + } + + this._renderObjectProperties(container, addedPromiseInternalProps); + this._renderObjectSuffix(); + } +}); // Widgets.ObjectRenderers.byClass.Promise + +/** + * The widget used for displaying generic JS object previews. + */ +Widgets.ObjectRenderers.add({ + byKind: "Object", + + render: function() + { + let { ownProperties, safeGetterValues } = this.objectActor.preview; + if ((!ownProperties && !safeGetterValues) || this.options.concise) { + this._renderConciseObject(); + return; + } + + this._renderObjectPrefix(); + this._renderObjectProperties(this.element, false); + this._renderObjectSuffix(); + }, +}); // Widgets.ObjectRenderers.byKind.Object + +/** + * The long string widget. + * + * @constructor + * @param object message + * The owning message. + * @param object longStringActor + * The LongStringActor to display. + */ +Widgets.LongString = function(message, longStringActor) +{ + Widgets.BaseWidget.call(this, message); + this.longStringActor = longStringActor; + this._onClick = this._onClick.bind(this); + this._onSubstring = this._onSubstring.bind(this); +}; + +Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * The LongStringActor displayed by the widget. + * @type object + */ + longStringActor: null, + + render: function() + { + if (this.element) { + return this; + } + + let result = this.element = this.document.createElementNS(XHTML_NS, "span"); + result.className = "longString console-string"; + this._renderString(this.longStringActor.initial); + result.appendChild(this._renderEllipsis()); + + return this; + }, + + /** + * Render the long string in the widget element. + * @private + * @param string str + * The string to display. + */ + _renderString: function(str) + { + this.element.textContent = VariablesView.getString(str, { + noStringQuotes: !this.message._quoteStrings, + noEllipsis: true, + }); + }, + + /** + * Render the anchor ellipsis that allows the user to expand the long string. + * + * @private + * @return Element + */ + _renderEllipsis: function() + { + let ellipsis = this.document.createElementNS(XHTML_NS, "a"); + ellipsis.className = "longStringEllipsis"; + ellipsis.textContent = l10n.getStr("longStringEllipsis"); + ellipsis.href = "#"; + ellipsis.draggable = false; + this.message._addLinkCallback(ellipsis, this._onClick); + + return ellipsis; + }, + + /** + * The click event handler for the ellipsis shown after the short string. This + * function expands the element to show the full string. + * @private + */ + _onClick: function() + { + let longString = this.output.webConsoleClient.longString(this.longStringActor); + let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH); + + longString.substring(longString.initial.length, toIndex, this._onSubstring); + }, + + /** + * The longString substring response callback. + * + * @private + * @param object response + * Response packet. + */ + _onSubstring: function(response) + { + if (response.error) { + Cu.reportError("LongString substring failure: " + response.error); + return; + } + + this.element.lastChild.remove(); + this.element.classList.remove("longString"); + + this._renderString(this.longStringActor.initial + response.substring); + + this.output.owner.emit("new-messages", new Set([{ + update: true, + node: this.message.element, + response: response, + }])); + + let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH); + if (toIndex != this.longStringActor.length) { + this._logWarningAboutStringTooLong(); + } + }, + + /** + * Inform user that the string he tries to view is too long. + * @private + */ + _logWarningAboutStringTooLong: function() + { + let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), { + category: "output", + severity: "warning", + }); + this.output.addMessage(msg); + }, +}); // Widgets.LongString.prototype + + +/** + * The stacktrace widget. + * + * @constructor + * @extends Widgets.BaseWidget + * @param object message + * The owning message. + * @param array stacktrace + * The stacktrace to display, array of frames as supplied by the server, + * over the remote protocol. + */ +Widgets.Stacktrace = function(message, stacktrace) +{ + Widgets.BaseWidget.call(this, message); + this.stacktrace = stacktrace; +}; + +Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * The stackframes received from the server. + * @type array + */ + stacktrace: null, + + render: function() + { + if (this.element) { + return this; + } + + let result = this.element = this.document.createElementNS(XHTML_NS, "ul"); + result.className = "stacktrace devtools-monospace"; + + for (let frame of this.stacktrace) { + result.appendChild(this._renderFrame(frame)); + } + + return this; + }, + + /** + * Render a frame object received from the server. + * + * @param object frame + * The stack frame to display. This object should have the following + * properties: functionName, filename and lineNumber. + * @return DOMElement + * The DOM element to display for the given frame. + */ + _renderFrame: function(frame) + { + let fn = this.document.createElementNS(XHTML_NS, "span"); + fn.className = "function"; + if (frame.functionName) { + let span = this.document.createElementNS(XHTML_NS, "span"); + span.className = "cm-variable"; + span.textContent = frame.functionName; + fn.appendChild(span); + fn.appendChild(this.document.createTextNode("()")); + } else { + fn.classList.add("cm-comment"); + fn.textContent = l10n.getStr("stacktrace.anonymousFunction"); + } + + let location = this.output.owner.createLocationNode({url: frame.filename, + line: frame.lineNumber}, + "jsdebugger"); + + // .devtools-monospace sets font-size to 80%, however .body already has + // .devtools-monospace. If we keep it here, the location would be rendered + // smaller. + location.classList.remove("devtools-monospace"); + + let elem = this.document.createElementNS(XHTML_NS, "li"); + elem.appendChild(fn); + elem.appendChild(location); + elem.appendChild(this.document.createTextNode("\n")); + + return elem; + }, +}); // Widgets.Stacktrace.prototype + + +/** + * The table widget. + * + * @constructor + * @extends Widgets.BaseWidget + * @param object message + * The owning message. + * @param array data + * Array of objects that holds the data to log in the table. + * @param object columns + * Object containing the key value pair of the id and display name for + * the columns in the table. + */ +Widgets.Table = function(message, data, columns) +{ + Widgets.BaseWidget.call(this, message); + this.data = data; + this.columns = columns; +}; + +Widgets.Table.prototype = Heritage.extend(Widgets.BaseWidget.prototype, +{ + /** + * Array of objects that holds the data to output in the table. + * @type array + */ + data: null, + + /** + * Object containing the key value pair of the id and display name for + * the columns in the table. + * @type object + */ + columns: null, + + render: function() { + if (this.element) { + return this; + } + + let result = this.element = this.document.createElementNS(XHTML_NS, "div"); + result.className = "consoletable devtools-monospace"; + + this.table = new TableWidget(result, { + initialColumns: this.columns, + uniqueId: "_index", + firstColumn: "_index" + }); + + for (let row of this.data) { + this.table.push(row); + } + + return this; + } +}); // Widgets.Table.prototype + +function gSequenceId() +{ + return gSequenceId.n++; +} +gSequenceId.n = 0; + +exports.ConsoleOutput = ConsoleOutput; +exports.Messages = Messages; +exports.Widgets = Widgets; diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/hudservice.js index 3eb54073b..e8ef0fe65 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/hudservice.js @@ -1,4 +1,4 @@ -/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ /* vim: set ft=javascript 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 @@ -6,35 +6,20 @@ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; +const {Cc, Ci, Cu} = require("chrome"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; +let Heritage = require("sdk/core/heritage"); -XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", - "resource:///modules/devtools/gDevTools.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "devtools", - "resource://gre/modules/devtools/Loader.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", - "resource://gre/modules/devtools/dbg-server.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", - "resource://gre/modules/devtools/dbg-client.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource://gre/modules/devtools/WebConsoleUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "promise", - "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise"); - -XPCOMUtils.defineLazyModuleGetter(this, "Heritage", - "resource:///modules/devtools/ViewHelpers.jsm"); +loader.lazyGetter(this, "Telemetry", () => require("devtools/shared/telemetry")); +loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame); +loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); +loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm"); +loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); +loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm"); +loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm"); +loader.lazyGetter(this, "showDoorhanger", () => require("devtools/shared/doorhanger").showDoorhanger); const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); @@ -44,32 +29,38 @@ const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,re // The preference prefix for all of the Browser Console filters. const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter."; -this.EXPORTED_SYMBOLS = ["HUDService"]; +let gHudId = 0; /////////////////////////////////////////////////////////////////////////// //// The HUD service function HUD_SERVICE() { - this.hudReferences = {}; + this.consoles = new Map(); + this.lastFinishedRequest = { callback: null }; } HUD_SERVICE.prototype = { + _browserConsoleID: null, + _browserConsoleDefer: null, + /** - * Keeps a reference for each HeadsUpDisplay that is created - * @type object + * Keeps a reference for each Web Console / Browser Console that is created. + * @type Map */ - hudReferences: null, + consoles: null, /** - * getter for UI commands to be used by the frontend + * Assign a function to this property to listen for every request that + * completes. Used by unit tests. The callback takes one argument: the HTTP + * activity object as received from the remote Web Console. * - * @returns object + * @type object + * Includes a property named |callback|. Assign the function to the + * |callback| property of this object. */ - get consoleUI() { - return HeadsUpDisplayUICommands; - }, + lastFinishedRequest: null, /** * Firefox-specific current tab getter @@ -98,14 +89,14 @@ HUD_SERVICE.prototype = function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow) { let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow); - this.hudReferences[hud.hudId] = hud; + this.consoles.set(hud.hudId, hud); return hud.init(); }, /** * Open a Browser Console for the given target. * - * @see devtools/framework/Target.jsm for details about targets. + * @see devtools/framework/target.js for details about targets. * * @param object aTarget * The target that the browser console will connect to. @@ -120,19 +111,20 @@ HUD_SERVICE.prototype = function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow) { let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow); - this.hudReferences[hud.hudId] = hud; + this._browserConsoleID = hud.hudId; + this.consoles.set(hud.hudId, hud); return hud.init(); }, /** - * Returns the HeadsUpDisplay object associated to a content window. + * Returns the Web Console object associated to a content window. * * @param nsIDOMWindow aContentWindow * @returns object */ getHudByWindow: function HS_getHudByWindow(aContentWindow) { - for each (let hud in this.hudReferences) { + for (let [hudId, hud] of this.consoles) { let target = hud.target; if (target && target.tab && target.window === aContentWindow) { return hud; @@ -142,37 +134,149 @@ HUD_SERVICE.prototype = }, /** - * Returns the hudId that is corresponding to the hud activated for the - * passed aContentWindow. If there is no matching hudId null is returned. + * Returns the console instance for a given id. * - * @param nsIDOMWindow aContentWindow - * @returns string or null + * @param string aId + * @returns Object */ - getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow) + getHudReferenceById: function HS_getHudReferenceById(aId) { - let hud = this.getHudByWindow(aContentWindow); - return hud ? hud.hudId : null; + return this.consoles.get(aId); }, /** - * Returns the hudReference for a given id. - * - * @param string aId - * @returns Object + * Find if there is a Web Console open for the current tab and return the + * instance. + * @return object|null + * The WebConsole object or null if the active tab has no open Web + * Console. */ - getHudReferenceById: function HS_getHudReferenceById(aId) + getOpenWebConsole: function HS_getOpenWebConsole() { - return aId in this.hudReferences ? this.hudReferences[aId] : null; + let tab = this.currentContext().gBrowser.selectedTab; + if (!tab || !devtools.TargetFactory.isKnownTab(tab)) { + return null; + } + let target = devtools.TargetFactory.forTab(tab); + let toolbox = gDevTools.getToolbox(target); + let panel = toolbox ? toolbox.getPanel("webconsole") : null; + return panel ? panel.hud : null; }, /** - * Assign a function to this property to listen for every request that - * completes. Used by unit tests. The callback takes one argument: the HTTP - * activity object as received from the remote Web Console. + * Toggle the Browser Console. + */ + toggleBrowserConsole: function HS_toggleBrowserConsole() + { + if (this._browserConsoleID) { + let hud = this.getHudReferenceById(this._browserConsoleID); + return hud.destroy(); + } + + if (this._browserConsoleDefer) { + return this._browserConsoleDefer.promise; + } + + this._browserConsoleDefer = promise.defer(); + + function connect() + { + let deferred = promise.defer(); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect(() => + client.listTabs((aResponse) => { + // Add Global Process debugging... + let globals = Cu.cloneInto(aResponse, {}); + delete globals.tabs; + delete globals.selected; + // ...only if there are appropriate actors (a 'from' property will + // always be there). + if (Object.keys(globals).length > 1) { + deferred.resolve({ form: globals, client: client, chrome: true }); + } else { + deferred.reject("Global console not found!"); + } + })); + + return deferred.promise; + } + + let target; + function getTarget(aConnection) + { + let options = { + form: aConnection.form, + client: aConnection.client, + chrome: true, + }; + + return devtools.TargetFactory.forRemoteTab(options); + } + + function openWindow(aTarget) + { + target = aTarget; + + let deferred = promise.defer(); + + let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank", + BROWSER_CONSOLE_WINDOW_FEATURES, null); + win.addEventListener("DOMContentLoaded", function onLoad() { + win.removeEventListener("DOMContentLoaded", onLoad); + + // Set the correct Browser Console title. + let root = win.document.documentElement; + root.setAttribute("title", root.getAttribute("browserConsoleTitle")); + + deferred.resolve(win); + }); + + return deferred.promise; + } + + connect().then(getTarget).then(openWindow).then((aWindow) => { + this.openBrowserConsole(target, aWindow, aWindow) + .then((aBrowserConsole) => { + this._browserConsoleDefer.resolve(aBrowserConsole); + this._browserConsoleDefer = null; + }) + }, console.error); + + return this._browserConsoleDefer.promise; + }, + + /** + * Opens or focuses the Browser Console. + */ + openBrowserConsoleOrFocus: function HS_openBrowserConsoleOrFocus() + { + let hud = this.getBrowserConsole(); + if (hud) { + hud.iframeWindow.focus(); + return promise.resolve(hud); + } + else { + return this.toggleBrowserConsole(); + } + }, + + /** + * Get the Browser Console instance, if open. * - * @type function + * @return object|null + * A BrowserConsole instance or null if the Browser Console is not + * open. */ - lastFinishedRequestCallback: null, + getBrowserConsole: function HS_getBrowserConsole() + { + return this.getHudReferenceById(this._browserConsoleID); + }, }; @@ -197,7 +301,7 @@ function WebConsole(aTarget, aIframeWindow, aChromeWindow) { this.iframeWindow = aIframeWindow; this.chromeWindow = aChromeWindow; - this.hudId = "hud_" + Date.now(); + this.hudId = "hud_" + ++gHudId; this.target = aTarget; this.browserWindow = this.chromeWindow.top; @@ -207,7 +311,7 @@ function WebConsole(aTarget, aIframeWindow, aChromeWindow) this.browserWindow = HUDService.currentContext(); } - this.ui = new this.iframeWindow.WebConsoleFrame(this); + this.ui = new WebConsoleFrame(this); } WebConsole.prototype = { @@ -221,12 +325,29 @@ WebConsole.prototype = { _destroyer: null, /** - * Getter for HUDService.lastFinishedRequestCallback. + * Getter for a function to to listen for every request that completes. Used + * by unit tests. The callback takes one argument: the HTTP activity object as + * received from the remote Web Console. * - * @see HUDService.lastFinishedRequestCallback * @type function */ - get lastFinishedRequestCallback() HUDService.lastFinishedRequestCallback, + get lastFinishedRequestCallback() HUDService.lastFinishedRequest.callback, + + /** + * Getter for the window that can provide various utilities that the web + * console makes use of, like opening links, managing popups, etc. In + * most cases, this will be |this.browserWindow|, but in some uses (such as + * the Browser Toolbox), there is no browser window, so an alternative window + * hosts the utilities there. + * @type nsIDOMWindow + */ + get chromeUtilsWindow() + { + if (this.browserWindow) { + return this.browserWindow; + } + return this.chromeWindow.top; + }, /** * Getter for the xul:popupset that holds any popups we open. @@ -234,7 +355,7 @@ WebConsole.prototype = { */ get mainPopupSet() { - return this.browserWindow.document.getElementById("mainPopupSet"); + return this.chromeUtilsWindow.document.getElementById("mainPopupSet"); }, /** @@ -246,7 +367,10 @@ WebConsole.prototype = { return this.ui ? this.ui.outputNode : null; }, - get gViewSourceUtils() this.browserWindow.gViewSourceUtils, + get gViewSourceUtils() + { + return this.chromeUtilsWindow.gViewSourceUtils; + }, /** * Initialize the Web Console instance. @@ -309,7 +433,7 @@ WebConsole.prototype = { */ openLink: function WC_openLink(aLink) { - this.browserWindow.openUILinkIn(aLink, "tab"); + this.chromeUtilsWindow.openUILinkIn(aLink, "tab"); }, /** @@ -370,55 +494,74 @@ WebConsole.prototype = { viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) { - let self = this; - let panelWin = null; - let debuggerWasOpen = true; let toolbox = gDevTools.getToolbox(this.target); if (!toolbox) { - self.viewSource(aSourceURL, aSourceLine); + this.viewSource(aSourceURL, aSourceLine); return; } - if (!toolbox.getPanel("jsdebugger")) { - debuggerWasOpen = false; - let toolboxWin = toolbox.doc.defaultView; - toolboxWin.addEventListener("Debugger:AfterSourcesAdded", - function afterSourcesAdded() { - toolboxWin.removeEventListener("Debugger:AfterSourcesAdded", - afterSourcesAdded); - loadScript(); - }); + let showSource = ({ DebuggerView }) => { + let item = DebuggerView.Sources.getItemForAttachment( + a => a.source.url === aSourceURL + ); + if (item) { + DebuggerView.setEditorLocation(item.attachment.source.actor, aSourceLine, + { noDebug: true }).then(() => { + this.ui.emit("source-in-debugger-opened"); + }); + return; + } + toolbox.selectTool("webconsole") + .then(() => this.viewSource(aSourceURL, aSourceLine)); } - toolbox.selectTool("jsdebugger").then(function onDebuggerOpen(dbg) { - panelWin = dbg.panelWin; - if (debuggerWasOpen) { - loadScript(); + // If the Debugger was already open, switch to it and try to show the + // source immediately. Otherwise, initialize it and wait for the sources + // to be added first. + let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger"); + toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { + if (debuggerAlreadyOpen) { + showSource(dbg); + } else { + dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); } }); + }, - function loadScript() { - let debuggerView = panelWin.DebuggerView; - if (!debuggerView.Sources.containsValue(aSourceURL)) { - toolbox.selectTool("webconsole"); - self.viewSource(aSourceURL, aSourceLine); - return; - } - if (debuggerWasOpen && debuggerView.Sources.selectedValue == aSourceURL) { - debuggerView.editor.setCaretPosition(aSourceLine - 1); + + /** + * Tries to open a JavaScript file related to the web page for the web console + * instance in the corresponding Scratchpad. + * + * @param string aSourceURL + * The URL of the file which corresponds to a Scratchpad id. + */ + viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL) + { + // Check for matching top level Scratchpad window. + let wins = Services.wm.getEnumerator("devtools:scratchpad"); + + while (wins.hasMoreElements()) { + let win = wins.getNext(); + + if (!win.closed && win.Scratchpad.uniqueName === aSourceURL) { + win.focus(); return; } - - panelWin.addEventListener("Debugger:SourceShown", onSource, false); - debuggerView.Sources.preferredSource = aSourceURL; } - function onSource(aEvent) { - if (aEvent.detail.url != aSourceURL) { - return; + // Check for matching Scratchpad toolbox tab. + for (let [, toolbox] of gDevTools) { + let scratchpadPanel = toolbox.getPanel("scratchpad"); + if (scratchpadPanel) { + let { scratchpad } = scratchpadPanel; + if (scratchpad.uniqueName === aSourceURL) { + toolbox.selectTool("scratchpad"); + toolbox.raise(); + scratchpad.editor.focus(); + return; + } } - panelWin.removeEventListener("Debugger:SourceShown", onSource, false); - panelWin.DebuggerView.editor.setCaretPosition(aSourceLine - 1); } }, @@ -445,18 +588,42 @@ WebConsole.prototype = { if (!panel) { return null; } - let framesController = panel.panelWin.gStackFrames; + let framesController = panel.panelWin.DebuggerController.StackFrames; let thread = framesController.activeThread; if (thread && thread.paused) { return { frames: thread.cachedFrames, - selected: framesController.currentFrame, + selected: framesController.currentFrameDepth, }; } return null; }, /** + * Retrieves the current selection from the Inspector, if such a selection + * exists. This is used to pass the ID of the selected actor to the Web + * Console server for the $0 helper. + * + * @return object|null + * A Selection referring to the currently selected node in the + * Inspector. + * If the inspector was never opened, or no node was ever selected, + * then |null| is returned. + */ + getInspectorSelection: function WC_getInspectorSelection() + { + let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + return null; + } + let panel = toolbox.getPanel("inspector"); + if (!panel || !panel.selection) { + return null; + } + return panel.selection; + }, + + /** * Destroy the object. Call this method to avoid memory leaks when the Web * Console is closed. * @@ -469,7 +636,7 @@ WebConsole.prototype = { return this._destroyer.promise; } - delete HUDService.hudReferences[this.hudId]; + HUDService.consoles.delete(this.hudId); this._destroyer = promise.defer(); @@ -525,6 +692,7 @@ WebConsole.prototype = { function BrowserConsole() { WebConsole.apply(this, arguments); + this._telemetry = new Telemetry(); } BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, @@ -555,6 +723,7 @@ BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, // instance. let onClose = () => { window.removeEventListener("unload", onClose); + window.removeEventListener("focus", onFocus); this.destroy(); }; window.addEventListener("unload", onClose); @@ -562,6 +731,14 @@ BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, // Make sure Ctrl-W closes the Browser Console window. window.document.getElementById("cmd_close").removeAttribute("disabled"); + this._telemetry.toolOpened("browserconsole"); + + // Create an onFocus handler just to display the dev edition promo. + // This is to prevent race conditions in some environments. + // Hook to display promotional Developer Edition doorhanger. Only displayed once. + let onFocus = () => showDoorhanger({ window, type: "deveditionpromo" }); + window.addEventListener("focus", onFocus); + this._bc_init = this.$init(); return this._bc_init; }, @@ -572,7 +749,7 @@ BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, * Destroy the object. * * @return object - * A Promise object that is resolved once the Browser Console is closed. + * A promise object that is resolved once the Browser Console is closed. */ destroy: function BC_destroy() { @@ -580,12 +757,14 @@ BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, return this._bc_destroyer.promise; } + this._telemetry.toolClosed("browserconsole"); + this._bc_destroyer = promise.defer(); let chromeWindow = this.chromeWindow; this.$destroy().then(() => this.target.client.close(() => { - HeadsUpDisplayUICommands._browserConsoleID = null; + HUDService._browserConsoleID = null; chromeWindow.close(); this._bc_destroyer.resolve(null); })); @@ -594,144 +773,17 @@ BrowserConsole.prototype = Heritage.extend(WebConsole.prototype, }, }); - -////////////////////////////////////////////////////////////////////////// -// HeadsUpDisplayUICommands -////////////////////////////////////////////////////////////////////////// - -var HeadsUpDisplayUICommands = { - _browserConsoleID: null, - _browserConsoleDefer: null, - - /** - * Toggle the Web Console for the current tab. - * - * @return object - * A promise for either the opening of the toolbox that holds the Web - * Console, or a promise for the closing of the toolbox. - */ - toggleHUD: function UIC_toggleHUD() - { - let window = HUDService.currentContext(); - let target = devtools.TargetFactory.forTab(window.gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); - - return toolbox && toolbox.currentToolId == "webconsole" ? - toolbox.destroy() : - gDevTools.showToolbox(target, "webconsole"); - }, - - /** - * Find if there is a Web Console open for the current tab and return the - * instance. - * @return object|null - * The WebConsole object or null if the active tab has no open Web - * Console. - */ - getOpenHUD: function UIC_getOpenHUD() - { - let tab = HUDService.currentContext().gBrowser.selectedTab; - if (!tab || !devtools.TargetFactory.isKnownTab(tab)) { - return null; - } - let target = devtools.TargetFactory.forTab(tab); - let toolbox = gDevTools.getToolbox(target); - let panel = toolbox ? toolbox.getPanel("webconsole") : null; - return panel ? panel.hud : null; - }, - - /** - * Toggle the Browser Console. - */ - toggleBrowserConsole: function UIC_toggleBrowserConsole() - { - if (this._browserConsoleID) { - let hud = HUDService.getHudReferenceById(this._browserConsoleID); - return hud.destroy(); - } - - if (this._browserConsoleDefer) { - return this._browserConsoleDefer.promise; - } - - this._browserConsoleDefer = promise.defer(); - - function connect() - { - let deferred = promise.defer(); - - if (!DebuggerServer.initialized) { - DebuggerServer.init(); - DebuggerServer.addBrowserActors(); - } - - let client = new DebuggerClient(DebuggerServer.connectPipe()); - client.connect(() => - client.listTabs((aResponse) => { - // Add Global Process debugging... - let globals = JSON.parse(JSON.stringify(aResponse)); - delete globals.tabs; - delete globals.selected; - // ...only if there are appropriate actors (a 'from' property will - // always be there). - if (Object.keys(globals).length > 1) { - deferred.resolve({ form: globals, client: client, chrome: true }); - } else { - deferred.reject("Global console not found!"); - } - })); - - return deferred.promise; - } - - let target; - function getTarget(aConnection) - { - let options = { - form: aConnection.form, - client: aConnection.client, - chrome: true, - }; - - return devtools.TargetFactory.forRemoteTab(options); - } - - function openWindow(aTarget) - { - target = aTarget; - - let deferred = promise.defer(); - - let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank", - BROWSER_CONSOLE_WINDOW_FEATURES, null); - win.addEventListener("DOMContentLoaded", function onLoad() { - win.removeEventListener("DOMContentLoaded", onLoad); - - // Set the correct Browser Console title. - let root = win.document.documentElement; - root.setAttribute("title", root.getAttribute("browserConsoleTitle")); - - deferred.resolve(win); - }); - - return deferred.promise; - } - - connect().then(getTarget).then(openWindow).then((aWindow) => - HUDService.openBrowserConsole(target, aWindow, aWindow) - .then((aBrowserConsole) => { - this._browserConsoleID = aBrowserConsole.hudId; - this._browserConsoleDefer.resolve(aBrowserConsole); - this._browserConsoleDefer = null; - })); - - return this._browserConsoleDefer.promise; - }, - - get browserConsole() { - return HUDService.getHudReferenceById(this._browserConsoleID); - }, -}; - const HUDService = new HUD_SERVICE(); +(() => { + let methods = ["openWebConsole", "openBrowserConsole", + "toggleBrowserConsole", "getOpenWebConsole", + "getBrowserConsole", "getHudByWindow", + "openBrowserConsoleOrFocus", "getHudReferenceById"]; + for (let method of methods) { + exports[method] = HUDService[method].bind(HUDService); + } + + exports.consoles = HUDService.consoles; + exports.lastFinishedRequest = HUDService.lastFinishedRequest; +})(); diff --git a/browser/devtools/webconsole/moz.build b/browser/devtools/webconsole/moz.build index 6ecebe123..6d42322e3 100644 --- a/browser/devtools/webconsole/moz.build +++ b/browser/devtools/webconsole/moz.build @@ -4,5 +4,13 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] - +EXTRA_JS_MODULES.devtools.webconsole += [ + 'console-commands.js', + 'console-output.js', + 'hudservice.js', + 'network-panel.js', + 'panel.js', + 'webconsole.js', +] diff --git a/browser/devtools/webconsole/NetworkPanel.jsm b/browser/devtools/webconsole/network-panel.js index e6d634f7b..626241b1d 100644 --- a/browser/devtools/webconsole/NetworkPanel.jsm +++ b/browser/devtools/webconsole/network-panel.js @@ -1,4 +1,4 @@ -/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ /* vim: set ft=javascript 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, @@ -6,28 +6,17 @@ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; +const {Cc, Ci, Cu} = require("chrome"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper")); +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService"); -XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", - "nsIMIMEService"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", - "resource://gre/modules/devtools/NetworkHelper.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); -this.EXPORTED_SYMBOLS = ["NetworkPanel"]; /** * Creates a new NetworkPanel. @@ -41,7 +30,6 @@ this.EXPORTED_SYMBOLS = ["NetworkPanel"]; * The parent WebConsoleFrame object that owns this network panel * instance. */ -this.NetworkPanel = function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame) { let doc = aParent.ownerDocument; @@ -108,6 +96,7 @@ function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame) aParent.appendChild(this.panel); } +exports.NetworkPanel = NetworkPanel; NetworkPanel.prototype = { @@ -297,7 +286,7 @@ NetworkPanel.prototype = return a.name.toLowerCase() < b.name.toLowerCase(); }); - aList.forEach(function(aItem) { + aList.forEach((aItem) => { let name = aItem.name; if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) { return; @@ -343,7 +332,7 @@ NetworkPanel.prototype = row.appendChild(td); parent.appendChild(row); - }.bind(this)); + }); }, /** @@ -623,7 +612,7 @@ NetworkPanel.prototype = let content = this.httpActivity.response.content; let longString = this.webconsole.webConsoleClient.longString(content.text); longString.substring(longString.initial.length, longString.length, - function NP__onLongStringSubstring(aResponse) + (aResponse) => { if (aResponse.error) { Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error); @@ -643,7 +632,7 @@ NetworkPanel.prototype = this._appendTextNode("responseBody" + cached + "Content", aResponse.substring); } - }.bind(this)); + }); }, /** @@ -790,7 +779,7 @@ NetworkPanel.prototype = let postData = this.httpActivity.request.postData; let longString = this.webconsole.webConsoleClient.longString(postData.text); longString.substring(longString.initial.length, longString.length, - function NP__onLongStringSubstring(aResponse) + (aResponse) => { if (aResponse.error) { Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error); @@ -799,7 +788,7 @@ NetworkPanel.prototype = postData.text = postData.text.initial + aResponse.substring; this._updateRequestBody(); - }.bind(this)); + }); }, }; diff --git a/browser/devtools/webconsole/WebConsolePanel.jsm b/browser/devtools/webconsole/panel.js index 03f421b42..f5c0e0cfd 100644 --- a/browser/devtools/webconsole/WebConsolePanel.jsm +++ b/browser/devtools/webconsole/panel.js @@ -4,34 +4,45 @@ "use strict"; -this.EXPORTED_SYMBOLS = [ "WebConsolePanel" ]; +const {Cc, Ci, Cu} = require("chrome"); -const { classes: Cc, interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "promise", - "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise"); - -XPCOMUtils.defineLazyModuleGetter(this, "HUDService", - "resource:///modules/HUDService.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource:///modules/devtools/shared/event-emitter.js"); +loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm"); +loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); +loader.lazyGetter(this, "HUDService", () => require("devtools/webconsole/hudservice")); +loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter")); +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); /** * A DevToolPanel that controls the Web Console. */ -function WebConsolePanel(iframeWindow, toolbox) { +function WebConsolePanel(iframeWindow, toolbox) +{ this._frameWindow = iframeWindow; this._toolbox = toolbox; EventEmitter.decorate(this); } +exports.WebConsolePanel = WebConsolePanel; + WebConsolePanel.prototype = { hud: null, /** + * Called by the WebConsole's onkey command handler. + * If the WebConsole is opened, check if the JSTerm's input line has focus. + * If not, focus it. + */ + focusInput: function WCP_focusInput() + { + let inputNode = this.hud.jsterm.inputNode; + + if (!inputNode.getAttribute("focused")) + { + inputNode.focus(); + } + }, + + /** * Open is effectively an asynchronous constructor. * * @return object @@ -41,7 +52,6 @@ WebConsolePanel.prototype = { { let parentDoc = this._toolbox.doc; let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole"); - iframe.className = "web-console-frame"; // Make sure the iframe content window is ready. let deferredIframe = promise.defer(); diff --git a/browser/devtools/webconsole/test/browser.ini b/browser/devtools/webconsole/test/browser.ini new file mode 100644 index 000000000..4dca6e15e --- /dev/null +++ b/browser/devtools/webconsole/test/browser.ini @@ -0,0 +1,380 @@ +[DEFAULT] +subsuite = devtools +support-files = + head.js + test-bug-585956-console-trace.html + test-bug-593003-iframe-wrong-hud-iframe.html + test-bug-593003-iframe-wrong-hud.html + test-bug-595934-canvas-css.html + test-bug-595934-canvas-css.js + test-bug-595934-css-loader.css + test-bug-595934-css-loader.css^headers^ + test-bug-595934-css-loader.html + test-bug-595934-css-parser.css + test-bug-595934-css-parser.html + test-bug-595934-empty-getelementbyid.html + test-bug-595934-empty-getelementbyid.js + test-bug-595934-html.html + test-bug-595934-image.html + test-bug-595934-image.jpg + test-bug-595934-imagemap.html + test-bug-595934-malformedxml-external.html + test-bug-595934-malformedxml-external.xml + test-bug-595934-malformedxml.xhtml + test-bug-595934-svg.xhtml + test-bug-595934-workers.html + test-bug-595934-workers.js + test-bug-597136-external-script-errors.html + test-bug-597136-external-script-errors.js + test-bug-597756-reopen-closed-tab.html + test-bug-599725-response-headers.sjs + test-bug-600183-charset.html + test-bug-600183-charset.html^headers^ + test-bug-601177-log-levels.html + test-bug-601177-log-levels.js + test-bug-603750-websocket.html + test-bug-603750-websocket.js + test-bug-613013-console-api-iframe.html + test-bug-618078-network-exceptions.html + test-bug-621644-jsterm-dollar.html + test-bug-630733-response-redirect-headers.sjs + test-bug-632275-getters.html + test-bug-632347-iterators-generators.html + test-bug-644419-log-limits.html + test-bug-646025-console-file-location.html + test-bug-658368-time-methods.html + test-bug-737873-mixedcontent.html + test-bug-752559-ineffective-iframe-sandbox-warning0.html + test-bug-752559-ineffective-iframe-sandbox-warning1.html + test-bug-752559-ineffective-iframe-sandbox-warning2.html + test-bug-752559-ineffective-iframe-sandbox-warning3.html + test-bug-752559-ineffective-iframe-sandbox-warning4.html + test-bug-752559-ineffective-iframe-sandbox-warning5.html + test-bug-752559-ineffective-iframe-sandbox-warning-inner.html + test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html + test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html + test-bug-762593-insecure-passwords-about-blank-web-console-warning.html + test-bug-762593-insecure-passwords-web-console-warning.html + test-bug-766001-console-log.js + test-bug-766001-js-console-links.html + test-bug-766001-js-errors.js + test-bug-782653-css-errors-1.css + test-bug-782653-css-errors-2.css + test-bug-782653-css-errors.html + test-bug-837351-security-errors.html + test-bug-846918-hsts-invalid-headers.html + test-bug-846918-hsts-invalid-headers.html^headers^ + test-bug-859170-longstring-hang.html + test-bug-869003-iframe.html + test-bug-869003-top-window.html + test-closure-optimized-out.html + test-closures.html + test-console-assert.html + test-console-count.html + test-console-count-external-file.js + test-console-extras.html + test-console-replaced-api.html + test-console.html + test-console-table.html + test-console-output-02.html + test-console-output-03.html + test-console-output-04.html + test-console-output-dom-elements.html + test-console-output-events.html + test-console-column.html + test-consoleiframes.html + test-certificate-messages.html + test-data.json + test-data.json^headers^ + test-duplicate-error.html + test-encoding-ISO-8859-1.html + test-error.html + test-eval-in-stackframe.html + test-file-location.js + test-filter.html + test-for-of.html + test-iframe-762593-insecure-form-action.html + test-iframe-762593-insecure-frame.html + test-iframe1.html + test-iframe2.html + test-iframe3.html + test-image.png + test-mixedcontent-securityerrors.html + test-mutation.html + test-network-request.html + test-network.html + test-observe-http-ajax.html + test-own-console.html + test-property-provider.html + test-repeated-messages.html + test-result-format-as-string.html + test-webconsole-error-observer.html + test_bug_770099_violation.html + test_bug_770099_violation.html^headers^ + test-autocomplete-in-stackframe.html + testscript.js + test-bug_923281_console_log_filter.html + test-bug_923281_test1.js + test-bug_923281_test2.js + test-bug_939783_console_trace_duplicates.html + test-bug-952277-highlight-nodes-in-vview.html + test-bug-609872-cd-iframe-parent.html + test-bug-609872-cd-iframe-child.html + test-bug-989025-iframe-parent.html + test-console-api-stackframe.html + test_bug_1010953_cspro.html^headers^ + test_bug_1010953_cspro.html + test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ + test_bug1045902_console_csp_ignore_reflected_xss_message.html + test_bug1092055_shouldwarn.js^headers^ + test_bug1092055_shouldwarn.js + test_bug1092055_shouldwarn.html + +[browser_bug1045902_console_csp_ignore_reflected_xss_message.js] +[browser_bug664688_sandbox_update_after_navigation.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (intermittent Linux debug) +[browser_bug_638949_copy_link_location.js] +[browser_bug_862916_console_dir_and_filter_off.js] +[browser_bug_865288_repeat_different_objects.js] +[browser_bug_865871_variables_view_close_on_esc_key.js] +[browser_bug_869003_inspect_cross_domain_object.js] +[browser_bug_871156_ctrlw_close_tab.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (intermittent Linux debug) +[browser_cached_messages.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests (expectUncaughtException) +[browser_console.js] +[browser_console_addonsdk_loader_exception.js] +[browser_console_clear_on_reload.js] +[browser_console_click_focus.js] +[browser_console_consolejsm_output.js] +[browser_console_copy_command.js] +[browser_console_dead_objects.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_copy_entire_message_context_menu.js] +[browser_console_error_source_click.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_filters.js] +[browser_console_iframe_messages.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_keyboard_accessibility.js] +[browser_console_log_inspectable_object.js] +[browser_console_native_getters.js] +[browser_console_navigation_marker.js] +[browser_console_nsiconsolemessage.js] +skip-if = buildapp == 'mulet' +[browser_console_optimized_out_vars.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_private_browsing.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_variables_view.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_variables_view_dom_nodes.js] +[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js] +skip-if = buildapp == 'mulet' +[browser_console_variables_view_while_debugging.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_variables_view_while_debugging_and_inspecting.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_eval_in_debugger_stackframe.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_eval_in_debugger_stackframe2.js] +[browser_jsterm_inspect.js] +[browser_longstring_hang.js] +[browser_netpanel_longstring_expand.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_output_breaks_after_console_dir_uninspectable.js] +[browser_output_longstring_expand.js] +[browser_repeated_messages_accuracy.js] +skip-if = buildapp == 'mulet' +[browser_result_format_as_string.js] +[browser_warn_user_about_replaced_api.js] +[browser_webconsole_abbreviate_source_url.js] +[browser_webconsole_allow_mixedcontent_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_assert.js] +[browser_webconsole_basic_net_logging.js] +[browser_webconsole_block_mixedcontent_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_579412_input_focus.js] +[browser_webconsole_bug_580001_closing_after_completion.js] +[browser_webconsole_bug_580030_errors_after_page_reload.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_580454_timestamp_l10n.js] +[browser_webconsole_bug_582201_duplicate_errors.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js] +[browser_webconsole_bug_585237_line_limit.js] +[browser_webconsole_bug_585956_console_trace.js] +[browser_webconsole_bug_585991_autocomplete_keys.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_585991_autocomplete_popup.js] +[browser_webconsole_bug_586388_select_all.js] +[browser_webconsole_bug_587617_output_copy.js] +[browser_webconsole_bug_588342_document_focus.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_588730_text_node_insertion.js] +[browser_webconsole_bug_588967_input_expansion.js] +[browser_webconsole_bug_589162_css_filter.js] +[browser_webconsole_bug_592442_closing_brackets.js] +[browser_webconsole_bug_593003_iframe_wrong_hud.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_594477_clickable_output.js] +[browser_webconsole_bug_594497_history_arrow_keys.js] +[browser_webconsole_bug_595223_file_uri.js] +[browser_webconsole_bug_595350_multiple_windows_and_tabs.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_595934_message_categories.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_597136_external_script_errors.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_597136_network_requests_from_chrome.js] +[browser_webconsole_bug_597460_filter_scroll.js] +[browser_webconsole_bug_597756_reopen_closed_tab.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_599725_response_headers.js] +[browser_webconsole_bug_600183_charset.js] +[browser_webconsole_bug_601177_log_levels.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_601352_scroll.js] +[browser_webconsole_bug_601667_filter_buttons.js] +[browser_webconsole_bug_602572_log_bodies_checkbox.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_603750_websocket.js] +[browser_webconsole_bug_611795.js] +[browser_webconsole_bug_613013_console_api_iframe.js] +[browser_webconsole_bug_613280_jsterm_copy.js] +[browser_webconsole_bug_613642_maintain_scroll.js] +[browser_webconsole_bug_613642_prune_scroll.js] +[browser_webconsole_bug_614793_jsterm_scroll.js] +[browser_webconsole_bug_618078_network_exceptions.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_618311_close_panels.js] +[browser_webconsole_bug_621644_jsterm_dollar.js] +[browser_webconsole_bug_622303_persistent_filters.js] +[browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js] +skip-if = os != "win" +[browser_webconsole_bug_630733_response_redirect_headers.js] +[browser_webconsole_bug_632275_getters_document_width.js] +[browser_webconsole_bug_632347_iterators_generators.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_632817.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_642108_pruneTest.js] +[browser_webconsole_autocomplete_and_selfxss.js] +[browser_webconsole_bug_644419_log_limits.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_646025_console_file_location.js] +[browser_webconsole_bug_651501_document_body_autocomplete.js] +[browser_webconsole_bug_653531_highlighter_console_helper.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_658368_time_methods.js] +[browser_webconsole_bug_659907_console_dir.js] +[browser_webconsole_bug_660806_history_nav.js] +[browser_webconsole_bug_664131_console_group.js] +[browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js] +[browser_webconsole_bug_704295.js] +[browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js] +[browser_webconsole_bug_737873_mixedcontent.js] +[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js] +skip-if = true # Bug 1110500 - mouse event failure in test +[browser_webconsole_bug_764572_output_open_url.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_766001_JS_Console_in_Debugger.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests (expectUncaughtException) +[browser_webconsole_bug_770099_violation.js] +[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_804845_ctrl_key_nav.js] +skip-if = os != "mac" +[browser_webconsole_bug_817834_add_edited_input_to_history.js] +[browser_webconsole_bug_837351_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_846918_hsts_invalid-headers.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js] +[browser_webconsole_filter_buttons_contextmenu.js] +[browser_webconsole_bug_1006027_message_timestamps_incorrect.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug intermittent) +[browser_webconsole_bug_1010953_cspro.js] +[browser_webconsole_certificate_messages.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_show_subresource_security_errors.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_cached_autocomplete.js] +[browser_webconsole_change_font_size.js] +[browser_webconsole_chrome.js] +[browser_webconsole_clickable_urls.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_closure_inspection.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_completion.js] +[browser_webconsole_console_extras.js] +[browser_webconsole_console_logging_api.js] +[browser_webconsole_count.js] +[browser_webconsole_dont_navigate_on_doubleclick.js] +[browser_webconsole_execution_scope.js] +[browser_webconsole_for_of.js] +[browser_webconsole_history.js] +[browser_webconsole_input_field_focus_on_panel_select.js] +[browser_webconsole_inspect-parsed-documents.js] +[browser_webconsole_js_input_expansion.js] +[browser_webconsole_jsterm.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_live_filtering_of_message_types.js] +[browser_webconsole_live_filtering_on_search_strings.js] +[browser_webconsole_message_node_id.js] +[browser_webconsole_netlogging.js] +[browser_webconsole_network_panel.js] +[browser_webconsole_notifications.js] +[browser_webconsole_open-links-without-callback.js] +[browser_webconsole_output_copy_newlines.js] +[browser_webconsole_output_order.js] +[browser_webconsole_property_provider.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_scratchpad_panel_link.js] +[browser_webconsole_split.js] +[browser_webconsole_split_escape_key.js] +[browser_webconsole_split_focus.js] +[browser_webconsole_split_persist.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_view_source.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_reflow.js] +[browser_webconsole_log_file_filter.js] +[browser_webconsole_expandable_timestamps.js] +[browser_webconsole_autocomplete_in_debugger_stackframe.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_autocomplete_popup_close_on_tab_switch.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js] +[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js] +[browser_webconsole_output_01.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests +[browser_webconsole_output_02.js] +[browser_webconsole_output_03.js] +[browser_webconsole_output_04.js] +[browser_webconsole_output_05.js] +[browser_webconsole_output_06.js] +[browser_webconsole_output_dom_elements_01.js] +[browser_webconsole_output_dom_elements_02.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_output_dom_elements_03.js] +[browser_webconsole_output_dom_elements_04.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_output_events.js] +[browser_webconsole_output_table.js] +[browser_console_variables_view_highlighter.js] +[browser_webconsole_start_netmon_first.js] +[browser_webconsole_console_trace_duplicates.js] +[browser_webconsole_cd_iframe.js] +[browser_webconsole_autocomplete_crossdomain_iframe.js] +[browser_webconsole_console_custom_styles.js] +[browser_webconsole_console_api_stackframe.js] +[browser_webconsole_column_numbers.js] +[browser_console_open_or_focus.js] diff --git a/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js b/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js new file mode 100644 index 000000000..27cbf8ad0 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js @@ -0,0 +1,59 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Description of the test: + * We are loading a file with the following CSP: + * 'reflected-xss filter' + * This directive is not supported, hence we confirm that + * the according message is displayed in the web console. + */ + +const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive and values will be ignored."; +const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/test/" + + "test_bug1045902_console_csp_ignore_reflected_xss_message.html"; + +let hud = undefined; + +let TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring reflected XSS (bug 1045902)"; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + hud = yield openConsole(); + + yield loadDocument(browser); + yield testViolationMessage(); + + hud = null; +}); + + +function loadDocument(browser) { + let deferred = promise.defer(); + + hud.jsterm.clearOutput() + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve(); + }, true); + content.location = TEST_FILE; + + return deferred.promise; +} + +function testViolationMessage() { + let deferred = promise.defer(); + let aOutputNode = hud.outputNode; + + return waitForSuccess({ + name: "Confirming that CSP logs messages to the console when 'reflected-xss' directive is used!", + validator: function() { + console.log(hud.outputNode.textContent); + let success = false; + success = hud.outputNode.textContent.indexOf(EXPECTED_RESULT) > -1; + return success; + } + }); +} diff --git a/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js new file mode 100644 index 000000000..2203e2209 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js @@ -0,0 +1,91 @@ +/* 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 if the JSTerm sandbox is updated when the user navigates from one +// domain to another, in order to avoid permission denied errors with a sandbox +// created for a different origin. + +"use strict"; + +let test = asyncTest(function* () { + const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/test/test-console.html"; + + yield loadTab(TEST_URI1); + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + hud.jsterm.execute("window.location.href"); + + info("wait for window.location.href"); + + let msgForLocation1 = { + webconsole: hud, + messages: [ + { + name: "window.location.href jsterm input", + text: "window.location.href", + category: CATEGORY_INPUT, + }, + { + name: "window.location.href result is displayed", + text: TEST_URI1, + category: CATEGORY_OUTPUT, + }, + ], + }; + + yield waitForMessages(msgForLocation1); + + // load second url + content.location = TEST_URI2; + yield loadBrowser(gBrowser.selectedBrowser); + + is(hud.outputNode.textContent.indexOf("Permission denied"), -1, + "no permission denied errors"); + + hud.jsterm.clearOutput(); + hud.jsterm.execute("window.location.href"); + + info("wait for window.location.href after page navigation"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "window.location.href jsterm input", + text: "window.location.href", + category: CATEGORY_INPUT, + }, + { + name: "window.location.href result is displayed", + text: TEST_URI2, + category: CATEGORY_OUTPUT, + }, + ], + }); + + is(hud.outputNode.textContent.indexOf("Permission denied"), -1, + "no permission denied errors"); + + gBrowser.goBack(); + + yield waitForSuccess({ + name: "go back", + validator: function() { + return content.location.href == TEST_URI1; + }, + }); + + hud.jsterm.clearOutput(); + executeSoon(() => { + hud.jsterm.execute("window.location.href"); + }); + + info("wait for window.location.href after goBack()"); + yield waitForMessages(msgForLocation1); + is(hud.outputNode.textContent.indexOf("Permission denied"), -1, + "no permission denied errors"); +}); diff --git a/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js new file mode 100644 index 000000000..40ae04394 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js @@ -0,0 +1,105 @@ +/* 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/. */ + +// Test for the "Copy link location" context menu item shown when you right +// click network requests in the output. + +"use strict"; + +let test = asyncTest(function* () { + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" + + "test/test-console.html?_date=" + Date.now(); + const COMMAND_NAME = "consoleCmd_copyURL"; + const CONTEXT_MENU_ID = "#menu_copyURL"; + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo"); + }); + + Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true); + + yield loadTab(TEST_URI); + let hud = yield openConsole(); + let output = hud.outputNode; + let menu = hud.iframeWindow.document.getElementById("output-contextmenu"); + + hud.jsterm.clearOutput(); + content.console.log("bug 638949"); + + // Test that the "Copy Link Location" command is disabled for non-network + // messages. + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug 638949", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + output.focus(); + let message = [...result.matched][0]; + + goUpdateCommand(COMMAND_NAME); + ok(!isEnabled(), COMMAND_NAME + " is disabled"); + + // Test that the "Copy Link Location" menu item is hidden for non-network + // messages. + message.scrollIntoView(); + + yield waitForContextMenu(menu, message, () => { + let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden; + ok(isHidden, CONTEXT_MENU_ID + " is hidden"); + }); + + hud.jsterm.clearOutput(); + content.location.reload(); // Reloading will produce network logging + + // Test that the "Copy Link Location" command is enabled and works + // as expected for any network-related message. + // This command should copy only the URL. + [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); + + output.focus(); + message = [...result.matched][0]; + hud.ui.output.selectMessage(message); + + goUpdateCommand(COMMAND_NAME); + ok(isEnabled(), COMMAND_NAME + " is enabled"); + + info("expected clipboard value: " + message.url); + + let deferred = promise.defer(); + + waitForClipboard((aData) => { return aData.trim() == message.url; }, + () => { goDoCommand(COMMAND_NAME); }, + () => { deferred.resolve(null); }, + () => { deferred.reject(null); }); + + yield deferred.promise; + + // Test that the "Copy Link Location" menu item is visible for network-related + // messages. + message.scrollIntoView(); + + yield waitForContextMenu(menu, message, () => { + let isVisible = !menu.querySelector(CONTEXT_MENU_ID).hidden; + ok(isVisible, CONTEXT_MENU_ID + " is visible"); + }); + + // Return whether "Copy Link Location" command is enabled or not. + function isEnabled() { + let controller = top.document.commandDispatcher + .getControllerForCommand(COMMAND_NAME); + return controller && controller.isCommandEnabled(COMMAND_NAME); + } +}); diff --git a/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js b/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js new file mode 100644 index 000000000..676fd5a93 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js @@ -0,0 +1,31 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that the output for console.dir() works even if Logging filter is off. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 862916"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + ok(hud, "web console opened"); + + hud.setFilterState("log", false); + registerCleanupFunction(() => hud.setFilterState("log", true)); + + hud.jsterm.execute("window.fooBarz = 'bug862916'; " + + "console.dir(window)"); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + ok(varView, "variables view object"); + + yield findVariableViewProperties(varView, [ + { name: "fooBarz", value: "bug862916" }, + ], { webconsole: hud }); +}); + diff --git a/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js new file mode 100644 index 000000000..a3e3a8abe --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js @@ -0,0 +1,63 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure messages are not considered repeated when console.log() +// is invoked with different objects, see bug 865288. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-repeated-messages.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + info("waiting for 3 console.log objects"); + + hud.jsterm.clearOutput(true); + hud.jsterm.execute("window.testConsoleObjects()"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "3 console.log messages", + text: "abba", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 3, + repeats: 1, + objects: true, + }], + }); + + let msgs = [...result.matched]; + is(msgs.length, 3, "3 message elements"); + + for (let i = 0; i < msgs.length; i++) { + info("test message element #" + i); + + let msg = msgs[i]; + let clickable = msg.querySelector(".message-body a"); + ok(clickable, "clickable object #" + i); + + msg.scrollIntoView(false); + yield clickObject(clickable, i); + } + + function* clickObject(obj, i) + { + executeSoon(() => { + EventUtils.synthesizeMouse(obj, 2, 2, {}, hud.iframeWindow); + }); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + ok(varView, "variables view fetched #" + i); + + yield findVariableViewProperties(varView, [ + { name: "id", value: "abba" + i }, + ], { webconsole: hud }); + } +}); + diff --git a/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js new file mode 100644 index 000000000..a4dd76dbe --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js @@ -0,0 +1,98 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that the variables view sidebar can be closed by pressing Escape in the +// web console. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +function test() +{ + let hud; + + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + let jsterm = hud.jsterm; + + let msg = yield execute("fooObj"); + ok(msg, "output message found"); + + let anchor = msg.querySelector("a"); + let body = msg.querySelector(".message-body"); + ok(anchor, "object anchor"); + ok(body, "message body"); + ok(body.textContent.contains('testProp: "testValue"'), "message text check"); + + msg.scrollIntoView(); + executeSoon(() => { + EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow); + }); + + let vviewVar = yield jsterm.once("variablesview-fetched"); + let vview = vviewVar._variablesView; + ok(vview, "variables view object"); + + let [result] = yield findVariableViewProperties(vviewVar, [ + { name: "testProp", value: "testValue" }, + ], { webconsole: hud }); + + let prop = result.matchedProp; + ok(prop, "matched the |testProp| property in the variables view"); + + vview.window.focus(); + + executeSoon(() => { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + yield jsterm.once("sidebar-closed"); + + jsterm.clearOutput(); + + msg = yield execute("window.location"); + ok(msg, "output message found"); + + body = msg.querySelector(".message-body"); + ok(body, "message body"); + anchor = msg.querySelector("a"); + ok(anchor, "object anchor"); + ok(body.textContent.contains("Location \u2192 http://example.com/browser/"), + "message text check"); + + msg.scrollIntoView(); + executeSoon(() => { + EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow) + }); + vviewVar = yield jsterm.once("variablesview-fetched"); + + vview = vviewVar._variablesView; + ok(vview, "variables view object"); + + yield findVariableViewProperties(vviewVar, [ + { name: "host", value: "example.com" }, + ], { webconsole: hud }); + + vview.window.focus(); + + msg.scrollIntoView(); + executeSoon(() => { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + + yield jsterm.once("sidebar-closed"); + } + + function execute(str) { + let deferred = promise.defer(); + hud.jsterm.execute(str, (msg) => { + deferred.resolve(msg); + }); + return deferred.promise; + } +} diff --git a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js new file mode 100644 index 000000000..8a5ad49af --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js @@ -0,0 +1,76 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that users can inspect objects logged from cross-domain iframes - +// bug 869003. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-869003-top-window.html"; + +let test = asyncTest(function* () { + // This test is slightly more involved: it opens the web console, then the + // variables view for a given object, it updates a property in the view and + // checks the result. We can get a timeout with debug builds on slower machines. + requestLongerTimeout(2); + + yield loadTab("data:text/html;charset=utf8,<p>hello"); + let hud = yield openConsole(); + + content.location = TEST_URI; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log message", + text: "foobar", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + objects: true, + }], + }); + + let msg = [...result.matched][0]; + ok(msg, "message element"); + + let body = msg.querySelector(".message-body"); + ok(body, "message body"); + + let clickable = result.clickableElements[0]; + ok(clickable, "clickable object found"); + ok(body.textContent.contains('{ hello: "world!",'), "message text check"); + + executeSoon(() => { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow) + }); + + let aVar = yield hud.jsterm.once("variablesview-fetched"); + ok(aVar, "variables view fetched"); + ok(aVar._variablesView, "variables view object"); + + [result] = yield findVariableViewProperties(aVar, [ + { name: "hello", value: "world!" }, + { name: "bug", value: 869003 }, + ], { webconsole: hud }); + + let prop = result.matchedProp; + ok(prop, "matched the |hello| property in the variables view"); + + // Check that property value updates work. + aVar = yield updateVariablesViewProperty({ + property: prop, + field: "value", + string: "'omgtest'", + webconsole: hud, + }); + + info("onFetchAfterUpdate"); + + yield findVariableViewProperties(aVar, [ + { name: "hello", value: "omgtest" }, + { name: "bug", value: 869003 }, + ], { webconsole: hud }); +}); + diff --git a/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js new file mode 100644 index 000000000..503103566 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js @@ -0,0 +1,79 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that Ctrl-W closes the Browser Console and that Ctrl-W closes the +// current tab when using the Web Console - bug 871156. + +"use strict"; + +let test = asyncTest(function* () { + const TEST_URI = "data:text/html;charset=utf8,<title>bug871156</title>\n" + + "<p>hello world"; + let firstTab = gBrowser.selectedTab; + + Services.prefs.setBoolPref("browser.tabs.animate", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.tabs.animate"); + }); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + ok(hud, "Web Console opened"); + + let tabClosed = promise.defer(); + let toolboxDestroyed = promise.defer(); + let tabSelected = promise.defer(); + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() { + gBrowser.tabContainer.removeEventListener("TabClose", onTabClose); + info("tab closed"); + tabClosed.resolve(null); + }); + + gBrowser.tabContainer.addEventListener("TabSelect", function onTabSelect() { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect); + if (gBrowser.selectedTab == firstTab) { + info("tab selected"); + tabSelected.resolve(null); + } + }); + + toolbox.once("destroyed", () => { + info("toolbox destroyed"); + toolboxDestroyed.resolve(null); + }); + + // Get out of the web console initialization. + executeSoon(() => { + EventUtils.synthesizeKey("w", { accelKey: true }); + }); + + + yield promise.all([tabClosed.promise, toolboxDestroyed.promise, + tabSelected.promise]); + info("promise.all resolved. start testing the Browser Console"); + + hud = yield HUDService.toggleBrowserConsole(); + ok(hud, "Browser Console opened"); + + let deferred = promise.defer(); + + Services.obs.addObserver(function onDestroy() { + Services.obs.removeObserver(onDestroy, "web-console-destroyed"); + ok(true, "the Browser Console closed"); + + deferred.resolve(null); + }, "web-console-destroyed", false); + + waitForFocus(() => { + EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow); + }, hud.iframeWindow); + + yield deferred.promise; +}); diff --git a/browser/devtools/webconsole/test/browser_cached_messages.js b/browser/devtools/webconsole/test/browser_cached_messages.js new file mode 100644 index 000000000..a9a98ac17 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_cached_messages.js @@ -0,0 +1,53 @@ +/* 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/. */ + +// Test to see if the cached messages are displayed when the console UI is opened. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-webconsole-error-observer.html"; + +function test() +{ + waitForExplicitFinish(); + + expectUncaughtException(); + + loadTab(TEST_URI).then(testOpenUI); +} + +function testOpenUI(aTestReopen) +{ + openConsole().then((hud) => { + waitForMessages({ + webconsole: hud, + messages: [ + { + text: "log Bazzle", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "error Bazzle", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + }, + { + text: "bazBug611032", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "cssColorBug611032", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + ], + }).then(() => { + closeConsole(gBrowser.selectedTab).then(() => { + aTestReopen && info("will reopen the Web Console"); + executeSoon(aTestReopen ? testOpenUI : finishTest); + }); + }); + }); +} diff --git a/browser/devtools/webconsole/test/browser_console.js b/browser/devtools/webconsole/test/browser_console.js new file mode 100644 index 000000000..df971ec0f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console.js @@ -0,0 +1,120 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the basic features of the Browser Console, bug 587757. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html?" + Date.now(); + +const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`; + +"use strict"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + + let opened = waitForConsole(); + + let hud = HUDService.getBrowserConsole(); + ok(!hud, "browser console is not open"); + info("wait for the browser console to open with ctrl-shift-j"); + EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window); + + hud = yield opened; + ok(hud, "browser console opened"); + + yield consoleOpened(hud); +}); + +function consoleOpened(hud) +{ + hud.jsterm.clearOutput(true); + + expectUncaughtException(); + executeSoon(() => { + foobarExceptionBug587757(); + }); + + // Add a message from a chrome window. + hud.iframeWindow.console.log("bug587757a"); + + // Add a message from a content window. + content.console.log("bug587757b"); + + // Test eval. + hud.jsterm.execute("document.location.href"); + + // Check for network requests. + let xhr = new XMLHttpRequest(); + xhr.onload = () => console.log("xhr loaded, status is: " + xhr.status); + xhr.open("get", TEST_URI, true); + xhr.send(); + + // Check for xhr error. + let xhrErr = new XMLHttpRequest(); + xhrErr.onload = () => console.log("xhr error loaded, status is: " + xhrErr.status); + xhrErr.open("get", TEST_XHR_ERROR_URI, true); + xhrErr.send(); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + name: "chrome window console.log() is displayed", + text: "bug587757a", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "content window console.log() is displayed", + text: "bug587757b", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "jsterm eval result", + text: "browser.xul", + category: CATEGORY_OUTPUT, + severity: SEVERITY_LOG, + }, + { + name: "exception message", + text: "foobarExceptionBug587757", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + name: "network message", + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_INFO, + isXhr: true, + }, + { + name: "xhr error message", + text: "404.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_ERROR, + isXhr: true, + }, + ], + }); +} + +function waitForConsole() { + let deferred = promise.defer(); + + Services.obs.addObserver(function observer(aSubject) { + Services.obs.removeObserver(observer, "web-console-created"); + aSubject.QueryInterface(Ci.nsISupportsString); + + let hud = HUDService.getBrowserConsole(); + ok(hud, "browser console is open"); + is(aSubject.data, hud.hudId, "notification hudId is correct"); + + executeSoon(() => deferred.resolve(hud)); + }, "web-console-created", false); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js new file mode 100644 index 000000000..bc485ebb1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js @@ -0,0 +1,90 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that exceptions from scripts loaded with the addon-sdk loader are +// opened correctly in View Source from the Browser Console. +// See bug 866950. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950"; + +function test() +{ + requestLongerTimeout(2); + + let webconsole, browserconsole; + + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab(TEST_URI); + webconsole = yield openConsole(tab); + ok(webconsole, "web console opened"); + + browserconsole = yield HUDService.toggleBrowserConsole(); + ok(browserconsole, "browser console opened"); + + // Cause an exception in a script loaded with the addon-sdk loader. + let toolbox = gDevTools.getToolbox(webconsole.target); + let oldPanels = toolbox._toolPanels; + toolbox._toolPanels = {}; // non-iterable + + function fixToolbox() { + toolbox._toolPanels = oldPanels; + } + + info("generate exception and wait for message"); + + executeSoon(() => { + executeSoon(fixToolbox); + expectUncaughtException(); + toolbox.getToolPanels(); + }); + + let [result] = yield waitForMessages({ + webconsole: browserconsole, + messages: [{ + text: "TypeError: this._toolPanels is not iterable", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); + + fixToolbox(); + + let msg = [...result.matched][0]; + ok(msg, "message element found"); + let locationNode = msg.querySelector(".message-location"); + ok(locationNode, "message location element found"); + + let title = locationNode.getAttribute("title"); + info("location node title: " + title); + isnot(title.indexOf(" -> "), -1, "error comes from a subscript"); + + let viewSource = browserconsole.viewSource; + let URL = null; + let clickPromise = promise.defer(); + browserconsole.viewSource = (aURL) => { + info("browserconsole.viewSource() was invoked: " + aURL); + URL = aURL; + clickPromise.resolve(null); + }; + + msg.scrollIntoView(); + EventUtils.synthesizeMouse(locationNode, 2, 2, {}, + browserconsole.iframeWindow); + + info("wait for click on locationNode"); + yield clickPromise; + + info("view-source url: " + URL); + ok(URL, "we have some source URL after the click"); + isnot(URL.indexOf("toolbox.js"), -1, "we have the expected view source URL"); + is(URL.indexOf("->"), -1, "no -> in the URL given to view-source"); + + browserconsole.viewSource = viewSource; + } +} diff --git a/browser/devtools/webconsole/test/browser_console_clear_on_reload.js b/browser/devtools/webconsole/test/browser_console_clear_on_reload.js new file mode 100644 index 000000000..c88fdc7d5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_clear_on_reload.js @@ -0,0 +1,54 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that clear output on page reload works - bug 705921. + +"use strict"; + +let test = asyncTest(function*() { + const PREF = "devtools.webconsole.persistlog"; + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + + Services.prefs.setBoolPref(PREF, false); + registerCleanupFunction(() => Services.prefs.clearUserPref(PREF)); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + ok(hud, "Web Console opened"); + + hud.jsterm.clearOutput(); + hud.jsterm.execute("console.log('foobarz1')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + BrowserReload(); + yield loadBrowser(gBrowser.selectedBrowser); + + hud.jsterm.execute("console.log('foobarz2')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-console.html", + category: CATEGORY_NETWORK, + }, + { + text: "foobarz2", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + is(hud.outputNode.textContent.indexOf("foobarz1"), -1, + "foobarz1 has been removed from output"); +}); diff --git a/browser/devtools/webconsole/test/browser_console_click_focus.js b/browser/devtools/webconsole/test/browser_console_click_focus.js new file mode 100644 index 000000000..30a18598c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_click_focus.js @@ -0,0 +1,55 @@ +/* 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 the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Dolske Digs Bacon", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let msg = [...result.matched][0]; + let outputItem = msg.querySelector(".message-body"); + ok(outputItem, "found a logged message"); + + let inputNode = hud.jsterm.inputNode; + ok(inputNode.getAttribute("focused"), "input node is focused, first"); + + let lostFocus = () => { + inputNode.removeEventListener("blur", lostFocus); + info("input node lost focus"); + } + + inputNode.addEventListener("blur", lostFocus); + + document.getElementById("urlbar").click(); + + ok(!inputNode.getAttribute("focused"), "input node is not focused"); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + + ok(inputNode.getAttribute("focused"), "input node is focused, second time") + + // test click-drags are not focusing the input element. + EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4}, + outputItem); + EventUtils.sendMouseEvent({type: "click", clientX: 15, clientY: 5}, + outputItem); + + todo(!inputNode.getAttribute("focused"), "input node is not focused after drag"); +}); + 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 000000000..0601c11a4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js @@ -0,0 +1,137 @@ +/* + * 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. + +"use strict"; + +let test = asyncTest(function*() { + let storage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + let console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console; + console.log("bug861338-log-cached"); + + let hud = yield HUDService.toggleBrowserConsole(); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "cached console.log message", + text: "bug861338-log-cached", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + hud.jsterm.clearOutput(true); + + function testTrace() { + console.trace(); + } + + 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.dir(document); + testTrace(); + console.timeEnd("foobarTimer"); + + info("wait for the Console.jsm messages"); + + let results = yield 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.+\{\s*bug851231prop:\s"bug851231value"\s*\}/, + 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: "testTrace", + }, + }, + { + name: "console.dir output", + consoleDir: /XULDocument\s+.+\s+chrome:\/\/.+\/browser\.xul/, + }, + { + name: "console.time output", + consoleTime: "foobarTimer", + }, + { + name: "console.timeEnd output", + consoleTimeEnd: "foobarTimer", + }, + ], + }); + + let consoleErrorMsg = results[3]; + ok(consoleErrorMsg, "console.error message element found"); + let clickable = consoleErrorMsg.clickableElements[0]; + ok(clickable, "clickable object found for console.error"); + + let deferred = promise.defer(); + + 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); + + deferred.resolve(aVar); + }; + + hud.jsterm.on("variablesview-fetched", onFetch); + + clickable.scrollIntoView(false); + + info("wait for variablesview-fetched"); + executeSoon(() => + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow)); + + let varView = yield deferred.promise; + ok(varView, "object inspector opened on click"); + + yield findVariableViewProperties(varView, [{ + name: "bug851231prop", + value: "bug851231value", + }], { webconsole: hud }); +}); diff --git a/browser/devtools/webconsole/test/browser_console_copy_command.js b/browser/devtools/webconsole/test/browser_console_copy_command.js new file mode 100644 index 000000000..711dfb5b5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_copy_command.js @@ -0,0 +1,70 @@ +/* vim: set ft=javascript 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/. */ + +// Tests that the `copy` console helper works as intended. + +let gWebConsole, gJSTerm; + +let TEXT = "Lorem ipsum dolor sit amet, consectetur adipisicing " + + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + + "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + new Date(); + +let ID = "select-me"; + +add_task(function* init() { + yield loadTab("data:text/html;charset=utf-8," + + "<body>" + + " <div>" + + " <h1>Testing copy command</h1>" + + " <p>This is some example text</p>" + + " <p id='select-me'>"+TEXT+"</p>" + + " </div>" + + " <div><p></p></div>" + + "</body>"); + + gWebConsole = yield openConsole(); + gJSTerm = gWebConsole.jsterm; +}); + +add_task(function* test_copy() { + let RANDOM = Math.random(); + let string = "Text: " + RANDOM; + let obj = {a: 1, b: "foo", c: RANDOM}; + + let samples = [[RANDOM, RANDOM], + [JSON.stringify(string), string], + [obj.toSource(), JSON.stringify(obj, null, " ")], + ["$('#" + ID + "')", content.document.getElementById(ID).outerHTML] + ]; + for (let [source, reference] of samples) { + let deferredResult = promise.defer(); + + SimpleTest.waitForClipboard( + "" + reference, + () => { + let command = "copy(" + source + ")"; + info("Attempting to copy: " + source); + info("Executing command: " + command); + gJSTerm.execute(command, msg => { + is(msg, undefined, "Command success: " + command); + }); + }, + deferredResult.resolve, + deferredResult.reject); + + yield deferredResult.promise; + } +}); + +add_task(function* cleanup() { + gWebConsole = gJSTerm = null; + gBrowser.removeTab(gBrowser.selectedTab); + finishTest(); +}); diff --git a/browser/devtools/webconsole/test/browser_console_copy_entire_message_context_menu.js b/browser/devtools/webconsole/test/browser_console_copy_entire_message_context_menu.js new file mode 100644 index 000000000..937793672 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_copy_entire_message_context_menu.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test copying of the entire console message when right-clicked +// with no other text selected. See Bug 1100562. + +function test() { + let hud; + let outputNode; + let contextMenu; + + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/")); + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + outputNode = hud.outputNode; + contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu"); + + registerCleanupFunction(() => { + hud = outputNode = contextMenu = null; + }); + + hud.jsterm.clearOutput(); + content.console.log("bug 1100562"); + + let [results] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug 1100562", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }] + }); + + outputNode.focus(); + let message = [...results.matched][0]; + message.scrollIntoView(); + + yield waitForContextMenu(contextMenu, message, copyFromPopup, testContextMenuCopy); + + function copyFromPopup() { + let copyItem = contextMenu.querySelector("#cMenu_copy"); + copyItem.doCommand(); + + let controller = top.document.commandDispatcher.getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled"); + } + + function testContextMenuCopy() { + waitForClipboard((str) => { return message.textContent.trim() == str.trim(); }, + () => { goDoCommand("cmd_copy") }, + () => {}, () => {} + ); + } + + yield closeConsole(tab); + } +}
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/browser_console_dead_objects.js b/browser/devtools/webconsole/test/browser_console_dead_objects.js new file mode 100644 index 000000000..574573064 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js @@ -0,0 +1,86 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that Dead Objects do not break the Web/Browser Consoles. See bug 883649. +// This test does: +// - opens a new tab, +// - opens the Browser Console, +// - stores a reference to the content document of the tab on the chrome window object, +// - closes the tab, +// - tries to use the object that was pointing to the now-defunct content +// document. This is the dead object. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>dead objects!"; + +function test() +{ + let hud = null; + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.chrome.enabled"); + }); + + Task.spawn(runner).then(finishTest); + + function* runner() { + Services.prefs.setBoolPref("devtools.chrome.enabled", true); + let {tab} = yield loadTab(TEST_URI); + + info("open the browser console"); + + hud = yield HUDService.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + let jsterm = hud.jsterm; + + jsterm.clearOutput(); + + // Add the reference to the content document. + yield jsterm.execute("Cu = Components.utils;" + + "Cu.import('resource://gre/modules/Services.jsm');" + + "chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');" + + "foobarzTezt = chromeWindow.content.document;" + + "delete chromeWindow"); + + gBrowser.removeCurrentTab(); + + let msg = yield jsterm.execute("foobarzTezt"); + + isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1, + "dead object found"); + + jsterm.setInputValue("foobarzTezt"); + + for (let c of ".hello") { + EventUtils.synthesizeKey(c, {}, hud.iframeWindow); + } + + yield jsterm.execute(); + + isnot(hud.outputNode.textContent.indexOf("can't access dead object"), -1, + "'cannot access dead object' message found"); + + // Click the second execute output. + let clickable = msg.querySelector("a"); + ok(clickable, "clickable object found"); + isnot(clickable.textContent.indexOf("[object DeadObject]"), -1, + "message text check"); + + msg.scrollIntoView(); + + executeSoon(() => { + EventUtils.synthesizeMouseAtCenter(clickable, {}, hud.iframeWindow); + }); + + yield jsterm.once("variablesview-fetched"); + ok(true, "variables view fetched"); + + msg = yield jsterm.execute("delete window.foobarzTezt; 2013-26"); + + isnot(msg.textContent.indexOf("1987"), -1, "result message found"); + } +} diff --git a/browser/devtools/webconsole/test/browser_console_error_source_click.js b/browser/devtools/webconsole/test/browser_console_error_source_click.js new file mode 100644 index 000000000..5e2ecb977 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js @@ -0,0 +1,73 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that JS errors and CSS warnings open view source when their source link +// is clicked in the Browser Console. See bug 877778. + +const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 877778 " + + "<button onclick='foobar.explode()' " + + "style='test-color: green-please'>click!</button>"; +function test() +{ + let hud; + + loadTab(TEST_URI).then(() => { + HUDService.toggleBrowserConsole().then(browserConsoleOpened); + }); + + function browserConsoleOpened(aHud) + { + hud = aHud; + ok(hud, "browser console opened"); + + let button = content.document.querySelector("button"); + ok(button, "button element found"); + + info("generate exception and wait for the message"); + executeSoon(() => { + expectUncaughtException(); + button.click(); + }); + + waitForMessages({ + webconsole: hud, + messages: [ + { + text: "ReferenceError: foobar is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "Unknown property 'test-color'", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + ], + }).then(onMessageFound); + } + + function onMessageFound(results) + { + let viewSource = hud.viewSource; + let viewSourceCalled = false; + hud.viewSource = () => viewSourceCalled = true; + + for (let result of results) { + viewSourceCalled = false; + + let msg = [...results[0].matched][0]; + ok(msg, "message element found for: " + result.text); + let locationNode = msg.querySelector(".message-location"); + ok(locationNode, "message location element found"); + + EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow); + + ok(viewSourceCalled, "view source opened"); + } + + hud.viewSource = viewSource; + finishTest(); + } +} diff --git a/browser/devtools/webconsole/test/browser_console_filters.js b/browser/devtools/webconsole/test/browser_console_filters.js new file mode 100644 index 000000000..1524b6bb1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_filters.js @@ -0,0 +1,60 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that the Browser Console does not use the same filter prefs as the Web +// Console. See bug 878186. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>browser console filters"; +const WEB_CONSOLE_PREFIX = "devtools.webconsole.filter."; +const BROWSER_CONSOLE_PREFIX = "devtools.browserconsole.filter."; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + + info("open the web console"); + let hud = yield openConsole(); + ok(hud, "web console opened"); + + is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (browser console)"); + is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (web console)"); + + info("toggle 'exception' filter"); + hud.setFilterState("exception", false); + + is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (browser console)"); + is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), false, + "'exception' filter is disabled (web console)"); + + hud.setFilterState("exception", true); + + // We need to let the console opening event loop to finish. + let deferred = promise.defer(); + executeSoon(() => closeConsole().then(() => deferred.resolve(null))); + yield deferred.promise; + + info("web console closed"); + hud = yield HUDService.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (browser console)"); + is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (web console)"); + + info("toggle 'exception' filter"); + hud.setFilterState("exception", false); + + is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), false, + "'exception' filter is disabled (browser console)"); + is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true, + "'exception' filter is enabled (web console)"); + + hud.setFilterState("exception", true); +}); diff --git a/browser/devtools/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js b/browser/devtools/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js new file mode 100644 index 000000000..c4f4cd836 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js @@ -0,0 +1,105 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* + * Bug 922161 - hide Browser Console JS input field if devtools.chrome.enabled is false + * when devtools.chrome.enabled then + * -browser console jsterm should be enabled + * -browser console object inspector properties should be set. + * -webconsole jsterm should be enabled + * -webconsole object inspector properties should be set. + * + * when devtools.chrome.enabled == false then + * -browser console jsterm should be disabled + * -browser console object inspector properties should not be set. + * -webconsole jsterm should be enabled + * -webconsole object inspector properties should be set. + */ + +function testObjectInspectorPropertiesAreNotSet(variablesView) { + is(variablesView.eval, null, "vview.eval is null"); + is(variablesView.switch, null, "vview.switch is null"); + is(variablesView.delete, null, "vview.delete is null"); +} + +function* getVariablesView(hud) { + function openVariablesView(event, vview) { + deferred.resolve(vview._variablesView); + } + + let deferred = promise.defer(); + hud.jsterm.clearOutput(); + hud.jsterm.execute('new Object()'); + + let [message] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Object", + category: CATEGORY_OUTPUT, + }], + }) + + hud.jsterm.once("variablesview-fetched", openVariablesView); + + let anchor = [...message.matched][0].querySelector("a"); + + executeSoon(() => + EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow) + ); + + return deferred.promise; +} + +function testJSTermIsVisible(hud) { + let inputContainer = hud.ui.window.document.querySelector(".jsterm-input-container"); + isnot(inputContainer.style.display, "none", "input is visible"); +} + +function testObjectInspectorPropertiesAreSet(variablesView) { + isnot(variablesView.eval, null, "vview.eval is set"); + isnot(variablesView.switch, null, "vview.switch is set"); + isnot(variablesView.delete, null, "vview.delete is set"); +} + +function testJSTermIsNotVisible(hud) { + let inputContainer = hud.ui.window.document.querySelector(".jsterm-input-container"); + is(inputContainer.style.display, "none", "input is not visible"); +} + +function* testRunner() { + let browserConsole, webConsole, variablesView; + + Services.prefs.setBoolPref("devtools.chrome.enabled", true); + + browserConsole = yield HUDService.toggleBrowserConsole(); + variablesView = yield getVariablesView(browserConsole); + testJSTermIsVisible(browserConsole); + testObjectInspectorPropertiesAreSet(variablesView); + + let {tab: browserTab} = yield loadTab("data:text/html;charset=utf8,hello world"); + webConsole = yield openConsole(browserTab); + variablesView = yield getVariablesView(webConsole); + testJSTermIsVisible(webConsole) + testObjectInspectorPropertiesAreSet(variablesView) + yield closeConsole(browserTab); + + yield HUDService.toggleBrowserConsole(); + Services.prefs.setBoolPref("devtools.chrome.enabled", false); + + browserConsole = yield HUDService.toggleBrowserConsole(); + variablesView = yield getVariablesView(browserConsole); + testJSTermIsNotVisible(browserConsole); + testObjectInspectorPropertiesAreNotSet(variablesView); + + webConsole = yield openConsole(browserTab); + variablesView = yield getVariablesView(webConsole); + testJSTermIsVisible(webConsole) + testObjectInspectorPropertiesAreSet(variablesView) + yield closeConsole(browserTab); +} + +function test() { + Task.spawn(testRunner).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_console_iframe_messages.js b/browser/devtools/webconsole/test/browser_console_iframe_messages.js new file mode 100644 index 000000000..f8e32b23a --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_iframe_messages.js @@ -0,0 +1,104 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that cached messages from nested iframes are displayed in the +// Web/Browser Console. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-consoleiframes.html"; + +const expectedMessages = [ + { + text: "main file", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "blah", + category: CATEGORY_JS, + severity: SEVERITY_ERROR + }, + { + text: "iframe 2", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: "iframe 3", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + } +]; + +// "iframe 1" console messages can be coalesced into one if they follow each +// other in the sequence of messages (depending on timing). If they do not, then +// they will be displayed in the console output independently, as separate +// messages. This is why we need to match any of the following two rules. +const expectedMessagesAny = [ + { + name: "iframe 1 (count: 2)", + text: "iframe 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2 + }, + { + name: "iframe 1 (repeats: 2)", + text: "iframe 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2 + }, +]; + +function test() +{ + expectUncaughtException(); + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(hud) +{ + ok(hud, "web console opened"); + + waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }).then(() => { + info("first messages matched"); + waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }).then(() => { + closeConsole().then(onWebConsoleClose); + }); + }); +} + +function onWebConsoleClose() +{ + info("web console closed"); + HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen); +} + +function onBrowserConsoleOpen(hud) +{ + ok(hud, "browser console opened"); + waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }).then(() => { + info("first messages matched"); + waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }).then(() => { + closeConsole().then(finishTest); + }); + }); +} diff --git a/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js new file mode 100644 index 000000000..bbf767314 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js @@ -0,0 +1,79 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that basic keyboard shortcuts work in the web console. + +"use strict"; + +let test = asyncTest(function*() { + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + ok(hud, "Web Console opened"); + + info("dump some spew into the console for scrolling"); + hud.jsterm.execute("(function() { for (var i = 0; i < 100; i++) { " + + "console.log('foobarz' + i);" + + "}})();"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz99", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let currentPosition = hud.outputNode.parentNode.scrollTop; + let bottom = currentPosition; + + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + isnot(hud.outputNode.parentNode.scrollTop, currentPosition, "scroll position changed after page up"); + + currentPosition = hud.outputNode.parentNode.scrollTop; + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + ok(hud.outputNode.parentNode.scrollTop > currentPosition, "scroll position now at bottom"); + + EventUtils.synthesizeKey("VK_HOME", {}); + is(hud.outputNode.parentNode.scrollTop, 0, "scroll position now at top"); + + EventUtils.synthesizeKey("VK_END", {}); + + let scrollTop = hud.outputNode.parentNode.scrollTop; + ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5, + "scroll position now at bottom"); + + info("try ctrl-l to clear output"); + executeSoon(() => { EventUtils.synthesizeKey("l", { ctrlKey: true }); }); + yield hud.jsterm.once("messages-cleared"); + + is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared"); + is(hud.jsterm.inputNode.getAttribute("focused"), "true", + "jsterm input is focused"); + + info("try ctrl-f to focus filter"); + EventUtils.synthesizeKey("F", { accelKey: true }); + ok(!hud.jsterm.inputNode.getAttribute("focused"), + "jsterm input is not focused"); + is(hud.ui.filterBox.getAttribute("focused"), "true", + "filter input is focused"); + + if (Services.appinfo.OS == "Darwin") { + ok(hud.ui.getFilterState("network"), "network category is enabled"); + EventUtils.synthesizeKey("t", { ctrlKey: true }); + ok(!hud.ui.getFilterState("network"), "accesskey for Network works"); + EventUtils.synthesizeKey("t", { ctrlKey: true }); + ok(hud.ui.getFilterState("network"), "accesskey for Network works (again)"); + } + else { + EventUtils.synthesizeKey("N", { altKey: true }); + let net = hud.ui.document.querySelector("toolbarbutton[category=net]"); + is(hud.ui.document.activeElement, net, + "accesskey for Network category focuses the Net button"); + } +}); diff --git a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js new file mode 100644 index 000000000..075f2fb70 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js @@ -0,0 +1,50 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that objects given to console.log() are inspectable. + +"use strict"; + +let test = asyncTest(function*() { + yield loadTab("data:text/html;charset=utf8,test for bug 676722 - inspectable objects for window.console"); + + let hud = yield openConsole(); + hud.jsterm.clearOutput(true); + + hud.jsterm.execute("myObj = {abba: 'omgBug676722'}"); + hud.jsterm.execute("console.log('fooBug676722', myObj)"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooBug676722", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + objects: true, + }], + }); + + let msg = [...result.matched][0]; + ok(msg, "message element"); + + let body = msg.querySelector(".message-body"); + ok(body, "message body"); + + let clickable = result.clickableElements[0]; + ok(clickable, "the console.log() object anchor was found"); + ok(body.textContent.contains('{ abba: "omgBug676722" }'), + "clickable node content is correct"); + + executeSoon(() => { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + }); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + ok(varView, "object inspector opened on click"); + + yield findVariableViewProperties(varView, [{ + name: "abba", + value: "omgBug676722", + }], { webconsole: hud }); +}); diff --git a/browser/devtools/webconsole/test/browser_console_native_getters.js b/browser/devtools/webconsole/test/browser_console_native_getters.js new file mode 100644 index 000000000..8213108ab --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_native_getters.js @@ -0,0 +1,99 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that native getters and setters for DOM elements work as expected in +// variables view - bug 870220. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<title>bug870220</title>\n" + + "<p>hello world\n<p>native getters!"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + let jsterm = hud.jsterm; + + jsterm.execute("document"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "HTMLDocument \u2192 data:text/html;charset=utf8", + category: CATEGORY_OUTPUT, + objects: true, + }], + }); + + let clickable = result.clickableElements[0]; + ok(clickable, "clickable object found"); + + executeSoon(() => { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + }); + + let fetchedVar = yield jsterm.once("variablesview-fetched"); + + let variablesView = fetchedVar._variablesView; + ok(variablesView, "variables view object"); + + let results = yield findVariableViewProperties(fetchedVar, [ + { name: "title", value: "bug870220" }, + { name: "bgColor" }, + ], { webconsole: hud }); + + let prop = results[1].matchedProp; + ok(prop, "matched the |bgColor| property in the variables view"); + + // Check that property value updates work. + let updatedVar = yield updateVariablesViewProperty({ + property: prop, + field: "value", + string: "'red'", + webconsole: hud, + }); + + info("on fetch after background update"); + + jsterm.clearOutput(true); + jsterm.execute("document.bgColor"); + + [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "red", + category: CATEGORY_OUTPUT, + }], + }); + + yield findVariableViewProperties(updatedVar, [ + { name: "bgColor", value: "red" }, + ], { webconsole: hud }); + + jsterm.execute("$$('p')"); + + [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "NodeList [", + category: CATEGORY_OUTPUT, + objects: true, + }], + }); + + clickable = result.clickableElements[0]; + ok(clickable, "clickable object found"); + + executeSoon(() => { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + }); + + fetchedVar = yield jsterm.once("variablesview-fetched"); + + yield findVariableViewProperties(fetchedVar, [ + { name: "0.textContent", value: /hello world/ }, + { name: "1.textContent", value: /native getters/ }, + ], { webconsole: hud }); +}); 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 000000000..280c525ae --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_navigation_marker.js @@ -0,0 +1,75 @@ +/* + * 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. + +const PREF = "devtools.webconsole.persistlog"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let hud; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref(PREF, true); + + let { browser } = yield loadTab(TEST_URI); + hud = yield openConsole(); + + yield consoleOpened(); + + let loaded = loadBrowser(browser); + BrowserReload(); + yield loaded; + + yield onReload(); + + isnot(hud.outputNode.textContent.indexOf("foobarz1"), -1, + "foobarz1 is still in the output"); + + Services.prefs.clearUserPref(PREF); + + hud = null; +}); + +function consoleOpened() +{ + ok(hud, "Web Console opened"); + + hud.jsterm.clearOutput(); + content.console.log("foobarz1"); + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +function onReload() +{ + content.console.log("foobarz2"); + + return 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, + }], + }); +} + diff --git a/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js b/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js new file mode 100644 index 000000000..ca87c057f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js @@ -0,0 +1,80 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that nsIConsoleMessages are displayed in the Browser Console. +// See bug 859756. + +const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" + + "<p>hello world\n<p>nsIConsoleMessages ftw!"; + +function test() +{ + const FILTER_PREF = "devtools.browserconsole.filter.jslog"; + Services.prefs.setBoolPref(FILTER_PREF, true); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref(FILTER_PREF); + }); + + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + + // Test for cached nsIConsoleMessages. + Services.console.logStringMessage("test1 for bug859756"); + + info("open web console"); + let hud = yield openConsole(tab); + + ok(hud, "web console opened"); + Services.console.logStringMessage("do-not-show-me"); + content.console.log("foobarz"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let text = hud.outputNode.textContent; + is(text.indexOf("do-not-show-me"), -1, + "nsIConsoleMessages are not displayed"); + is(text.indexOf("test1 for bug859756"), -1, + "nsIConsoleMessages are not displayed (confirmed)"); + + yield closeConsole(tab); + + info("web console closed"); + hud = yield HUDService.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + Services.console.logStringMessage("test2 for bug859756"); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test1 for bug859756", + category: CATEGORY_JS, + }, { + text: "test2 for bug859756", + category: CATEGORY_JS, + }, { + text: "do-not-show-me", + category: CATEGORY_JS, + }], + }); + + let msg = [...results[2].matched][0]; + ok(msg, "message element for do-not-show-me (nsIConsoleMessage)"); + isnot(msg.textContent.indexOf("do-not-show"), -1, "element content is correct"); + ok(!msg.classList.contains("filtered-by-type"), "element is not filtered"); + + hud.setFilterState("jslog", false); + + ok(msg.classList.contains("filtered-by-type"), "element is filtered"); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_console_open_or_focus.js b/browser/devtools/webconsole/test/browser_console_open_or_focus.js new file mode 100644 index 000000000..94625bc10 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_open_or_focus.js @@ -0,0 +1,47 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that the "browser console" menu item opens or focuses (if already open) +// the console window instead of toggling it open/close. + + +"use strict"; + +let test = asyncTest(function* () { + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let currWindow, hud, mainWindow; + + mainWindow = Services.wm.getMostRecentWindow(null); + + yield HUDService.openBrowserConsoleOrFocus(); + + hud = HUDService.getBrowserConsole(); + + console.log("testmessage"); + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "testmessage" + }], + }); + + currWindow = Services.wm.getMostRecentWindow(null); + is(currWindow.document.documentURI, devtools.Tools.webConsole.url, + "The Browser Console is open and has focus"); + + mainWindow.focus(); + + yield HUDService.openBrowserConsoleOrFocus(); + + currWindow = Services.wm.getMostRecentWindow(null); + is(currWindow.document.documentURI, devtools.Tools.webConsole.url, + "The Browser Console is open and has focus"); + + yield HUDService.toggleBrowserConsole(); + + hud = HUDService.getBrowserConsole(); + ok(!hud, "Browser Console has been closed"); +}); diff --git a/browser/devtools/webconsole/test/browser_console_optimized_out_vars.js b/browser/devtools/webconsole/test/browser_console_optimized_out_vars.js new file mode 100644 index 000000000..c3eceedf0 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_optimized_out_vars.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that inspecting an optimized out variable works when execution is +// paused. + +function test() { + Task.spawn(function* () { + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-closure-optimized-out.html"; + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let { toolbox, panel, panelWin } = yield openDebugger(); + + yield waitForThreadEvents(panel, "resumed"); + ok(true, "Debugger resumed"); + + let sources = panelWin.DebuggerView.Sources; + yield panel.addBreakpoint({ actor: sources.values[0], line: 18 }); + yield ensureThreadClientState(panel, "resumed"); + + let fetchedScopes = panelWin.once(panelWin.EVENTS.FETCHED_SCOPES); + let button = content.document.querySelector("button"); + ok(button, "Button element found"); + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => button.click()); + + let packet = yield fetchedScopes; + ok(true, "Scopes were fetched"); + + yield toolbox.selectTool("webconsole"); + + // This is the meat of the test: evaluate the optimized out variable. + hud.jsterm.execute("upvar"); + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "optimized out", + category: CATEGORY_OUTPUT, + }] + }); + + finishTest(); + }).then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); +} + +// Debugger helper functions stolen from browser/devtools/debugger/test/head.js. + +function ensureThreadClientState(aPanel, aState) { + let thread = aPanel.panelWin.gThreadClient; + let state = thread.state; + + info("Thread is: '" + state + "'."); + + if (state == aState) { + return promise.resolve(null); + } else { + return waitForThreadEvents(aPanel, aState); + } +} + +function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let thread = aPanel.panelWin.gThreadClient; + let count = 0; + + thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) { + info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' thread events have been fired."); + thread.removeListener(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_console_private_browsing.js b/browser/devtools/webconsole/test/browser_console_private_browsing.js new file mode 100644 index 000000000..f517fdd2c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_private_browsing.js @@ -0,0 +1,200 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Bug 874061: test for how the browser and web consoles display messages coming +// from private windows. See bug for description of expected behavior. + +function test() +{ + const TEST_URI = "data:text/html;charset=utf8,<p>hello world! bug 874061" + + "<button onclick='console.log(\"foobar bug 874061\");" + + "fooBazBaz.yummy()'>click</button>"; + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage); + let privateWindow, privateBrowser, privateTab, privateContent; + let hud, expectedMessages, nonPrivateMessage; + + // This test is slightly more involved: it opens the web console twice, + // a new private window once, and the browser console twice. We can get + // a timeout with debug builds on slower machines. + requestLongerTimeout(2); + start(); + + function start() + { + gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf8," + + "<p>hello world! I am not private!"); + gBrowser.selectedBrowser.addEventListener("load", onLoadTab, true); + } + + function onLoadTab() + { + gBrowser.selectedBrowser.removeEventListener("load", onLoadTab, true); + info("onLoadTab()"); + + // Make sure we have a clean state to start with. + Services.console.reset(); + ConsoleAPIStorage.clearEvents(); + + // Add a non-private message to the browser console. + content.console.log("bug874061-not-private"); + + nonPrivateMessage = { + name: "console message from a non-private window", + text: "bug874061-not-private", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }; + + privateWindow = OpenBrowserWindow({ private: true }); + ok(privateWindow, "new private window"); + ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window is private"); + + whenDelayedStartupFinished(privateWindow, onPrivateWindowReady); + } + + function onPrivateWindowReady() + { + info("private browser window opened"); + privateBrowser = privateWindow.gBrowser; + + privateTab = privateBrowser.selectedTab = privateBrowser.addTab(TEST_URI); + privateBrowser.selectedBrowser.addEventListener("load", function onLoad() { + info("private tab opened"); + privateBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + privateContent = privateBrowser.selectedBrowser.contentWindow; + ok(PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser), "tab window is private"); + openConsole(privateTab).then(consoleOpened); + }, true); + } + + function addMessages() + { + let button = privateContent.document.querySelector("button"); + ok(button, "button in page"); + EventUtils.synthesizeMouse(button, 2, 2, {}, privateContent); + } + + function consoleOpened(aHud) + { + hud = aHud; + ok(hud, "web console opened"); + + addMessages(); + expectedMessages = [ + { + name: "script error", + text: "fooBazBaz is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + name: "console message", + text: "foobar bug 874061", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + ]; + + // Make sure messages are displayed in the web console as they happen, even + // if this is a private tab. + waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }).then(testCachedMessages); + } + + function testCachedMessages() + { + info("testCachedMessages()"); + closeConsole(privateTab).then(() => { + info("web console closed"); + openConsole(privateTab).then(consoleReopened); + }); + } + + function consoleReopened(aHud) + { + hud = aHud; + ok(hud, "web console reopened"); + + // Make sure that cached messages are displayed in the web console, even + // if this is a private tab. + waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }).then(testBrowserConsole); + } + + function testBrowserConsole() + { + info("testBrowserConsole()"); + closeConsole(privateTab).then(() => { + info("web console closed"); + privateWindow.HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen); + }); + } + + // Make sure that the cached messages from private tabs are not displayed in + // the browser console. + function checkNoPrivateMessages() + { + let text = hud.outputNode.textContent; + is(text.indexOf("fooBazBaz"), -1, "no exception displayed"); + is(text.indexOf("bug 874061"), -1, "no console message displayed"); + } + + function onBrowserConsoleOpen(aHud) + { + hud = aHud; + ok(hud, "browser console opened"); + + checkNoPrivateMessages(); + addMessages(); + expectedMessages.push(nonPrivateMessage); + + // Make sure that live messages are displayed in the browser console, even + // from private tabs. + waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }).then(testPrivateWindowClose); + } + + function testPrivateWindowClose() + { + info("close the private window and check if the private messages are removed"); + hud.jsterm.once("private-messages-cleared", () => { + isnot(hud.outputNode.textContent.indexOf("bug874061-not-private"), -1, + "non-private messages are still shown after private window closed"); + checkNoPrivateMessages(); + + info("close the browser console"); + privateWindow.HUDService.toggleBrowserConsole().then(() => { + info("reopen the browser console"); + executeSoon(() => + HUDService.toggleBrowserConsole().then(onBrowserConsoleReopen)); + }); + }); + privateWindow.BrowserTryToCloseWindow(); + } + + function onBrowserConsoleReopen(aHud) + { + hud = aHud; + ok(hud, "browser console reopened"); + + // Make sure that the non-private message is still shown after reopen. + waitForMessages({ + webconsole: hud, + messages: [nonPrivateMessage], + }).then(() => { + // Make sure that no private message is displayed after closing the private + // window and reopening the Browser Console. + checkNoPrivateMessages(); + executeSoon(finishTest); + }); + } +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view.js b/browser/devtools/webconsole/test/browser_console_variables_view.js new file mode 100644 index 000000000..bfd5a128a --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view.js @@ -0,0 +1,189 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that variables view works as expected in the web console. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gVariablesView; + +let hud; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + + gWebConsole = hud; + gJSTerm = hud.jsterm; + let msg = yield gJSTerm.execute("fooObj"); + + ok(msg, "output message found"); + ok(msg.textContent.contains('{ testProp: "testValue" }'), "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + let fetched = gJSTerm.once("variablesview-fetched"); + + // executeSoon + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow); + + let view = yield fetched; + + let results = yield onFooObjFetch(view); + + let vView = yield onTestPropFound(results); + let results2 = yield onFooObjFetchAfterUpdate(vView); + + let vView2 = yield onUpdatedTestPropFound(results2); + let results3 = yield onFooObjFetchAfterPropRename(vView2); + + let vView3 = yield onRenamedTestPropFound(results3); + let results4 = yield onPropUpdateError(vView3); + + yield onRenamedTestPropFoundAgain(results4); + + let prop = results4[0].matchedProp; + yield testPropDelete(prop); + + gWebConsole = gJSTerm = gVariablesView = null; +}); + +function onFooObjFetch(aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + return findVariableViewProperties(aVar, [ + { name: "testProp", value: "testValue" }, + ], { webconsole: gWebConsole }); +} + +function onTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the |testProp| property in the variables view"); + + is("testValue", aResults[0].value, + "|fooObj.testProp| value is correct"); + + // Check that property value updates work and that jsterm functions can be + // used. + return updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + window.location + $('p')", + webconsole: gWebConsole + }); +} + +function onFooObjFetchAfterUpdate(aVar) +{ + info("onFooObjFetchAfterUpdate"); + let expectedValue = content.document.title + content.location + + '[object HTMLParagraphElement]'; + + return findVariableViewProperties(aVar, [ + { name: "testProp", value: expectedValue }, + ], { webconsole: gWebConsole }); +} + +function onUpdatedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the updated |testProp| property value"); + + is(content.wrappedJSObject.fooObj.testProp, aResults[0].value, + "|fooObj.testProp| value has been updated"); + + // Check that property name updates work. + return updateVariablesViewProperty({ + property: prop, + field: "name", + string: "testUpdatedProp", + webconsole: gWebConsole + }); +} + +function onFooObjFetchAfterPropRename(aVar) +{ + info("onFooObjFetchAfterPropRename"); + + let para = content.wrappedJSObject.document.querySelector("p"); + let expectedValue = content.document.title + content.location + para; + + // Check that the new value is in the variables view. + return findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: gWebConsole }); +} + +function onRenamedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the renamed |testProp| property"); + + ok(!content.wrappedJSObject.fooObj.testProp, + "|fooObj.testProp| has been deleted"); + is(content.wrappedJSObject.fooObj.testUpdatedProp, aResults[0].value, + "|fooObj.testUpdatedProp| is correct"); + + // Check that property value updates that cause exceptions are reported in + // the web console output. + return updateVariablesViewProperty({ + property: prop, + field: "value", + string: "foobarzFailure()", + webconsole: gWebConsole + }); +} + +function onPropUpdateError(aVar) +{ + info("onPropUpdateError"); + + let para = content.wrappedJSObject.document.querySelector("p"); + let expectedValue = content.document.title + content.location + para; + + // Make sure the property did not change. + return findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: gWebConsole }); +} + +function onRenamedTestPropFoundAgain(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the renamed |testProp| property again"); + + let outputNode = gWebConsole.outputNode; + + return waitForMessages({ + webconsole: gWebConsole, + messages: [{ + name: "exception in property update reported in the web console output", + text: "foobarzFailure", + category: CATEGORY_OUTPUT, + severity: SEVERITY_ERROR, + }], + }); +} + +function testPropDelete(aProp) +{ + gVariablesView.window.focus(); + aProp.focus(); + + executeSoon(() => { + EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window); + }); + + return waitForSuccess({ + name: "property deleted", + timeout: 60000, + validator: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj) + }); +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_dom_nodes.js b/browser/devtools/webconsole/test/browser_console_variables_view_dom_nodes.js new file mode 100644 index 000000000..318392bdf --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_dom_nodes.js @@ -0,0 +1,56 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* Test that ensures DOM nodes are rendered correctly in VariablesView. */ + +function test() { + const TEST_URI = 'data:text/html;charset=utf-8, \ + <html> \ + <head> \ + <title>Test for DOM nodes in variables view</title> \ + </head> \ + <body> \ + <div></div> \ + <div id="testID"></div> \ + <div class="single-class"></div> \ + <div class="multiple-classes another-class"></div> \ + <div class="class-and-id" id="class-and-id"></div> \ + <div class="multiple-classes-and-id another-class" \ + id="multiple-classes-and-id"></div> \ + <div class=" whitespace-start"></div> \ + <div class="whitespace-end "></div> \ + <div class="multiple spaces"></div> \ + </body> \ + </html>'; + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + const jsterm = hud.jsterm; + + let deferred = promise.defer(); + jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar)); + jsterm.execute("inspect(document.querySelectorAll('div'))"); + + let variableScope = yield deferred.promise; + ok(variableScope, "Variables view opened"); + + yield findVariableViewProperties(variableScope, [ + { name: "0", value: "<div>"}, + { name: "1", value: "<div#testID>"}, + { name: "2", value: "<div.single-class>"}, + { name: "3", value: "<div.multiple-classes.another-class>"}, + { name: "4", value: "<div#class-and-id.class-and-id>"}, + { name: "5", value: "<div#multiple-classes-and-id.multiple-classes-and-id.another-class>"}, + { name: "6", value: "<div.whitespace-start>"}, + { name: "7", value: "<div.whitespace-end>"}, + { name: "8", value: "<div.multiple.spaces>"}, + ], { webconsole: hud}); + + } +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js b/browser/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js new file mode 100644 index 000000000..60d82f99e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js @@ -0,0 +1,101 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* Test case that ensures Array and other list types are not sorted in variables + * view. + * + * The tested types are: + * - Array + * - Int8Array + * - Int16Array + * - Int32Array + * - Uint8Array + * - Uint16Array + * - Uint32Array + * - Uint8ClampedArray + * - Float32Array + * - Float64Array + * - NodeList + */ + +function test() { + const TEST_URI = "data:text/html;charset=utf-8, \ + <html> \ + <head> \ + <title>Test document for bug 977500</title> \ + </head> \ + <body> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + <div></div> \ + </body> \ + </html>"; + + let jsterm; + + function* runner() { + const typedArrayTypes = ["Int8Array", "Int16Array", "Int32Array", + "Uint8Array", "Uint16Array", "Uint32Array", + "Uint8ClampedArray", "Float32Array", + "Float64Array"]; + + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + jsterm = hud.jsterm; + + // Create an ArrayBuffer of 80 bytes to test TypedArrays. 80 bytes is + // enough to get 10 items in all different TypedArrays. + yield jsterm.execute("let buf = new ArrayBuffer(80);"); + + // Array + yield testNotSorted("Array(0,1,2,3,4,5,6,7,8,9,10)"); + // NodeList + yield testNotSorted("document.querySelectorAll('div')"); + + // Typed arrays. + for (let type of typedArrayTypes) { + yield testNotSorted("new " + type + "(buf)"); + } + } + + /** + * A helper that ensures the properties are not sorted when an object + * specified by aObject is inspected. + * + * @param string aObject + * A string that, once executed, creates and returns the object to + * inspect. + */ + function testNotSorted(aObject) { + info("Testing " + aObject); + let deferred = promise.defer(); + jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar)); + jsterm.execute("inspect(" + aObject + ")"); + + let variableScope = yield deferred.promise; + ok(variableScope, "Variables view opened"); + + // If the properties are sorted: keys = ["0", "1", "10",...] <- incorrect + // If the properties are not sorted: keys = ["0", "1", "2",...] <- correct + let keyIterator = variableScope._store.keys(); + is(keyIterator.next().value, "0", "First key is 0"); + is(keyIterator.next().value, "1", "Second key is 1"); + + // If the properties are sorted, the next one will be 10. + is(keyIterator.next().value, "2", "Third key is 2, not 10"); + } + + Task.spawn(runner).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_highlighter.js b/browser/devtools/webconsole/test/browser_console_variables_view_highlighter.js new file mode 100644 index 000000000..6c7087a94 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_highlighter.js @@ -0,0 +1,97 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that variables view is linked to the inspector for highlighting and +// selecting DOM nodes + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html"; + +let gWebConsole, gJSTerm, gVariablesView, gToolbox; + +function test() +{ + loadTab(TEST_URI).then(() => { + openConsole().then(hud => { + consoleOpened(hud); + }) + }); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gToolbox = gDevTools.getToolbox(hud.target); + gJSTerm.execute("document.querySelectorAll('p')").then(onQSAexecuted); +} + +function onQSAexecuted(msg) +{ + ok(msg, "output message found"); + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + gJSTerm.once("variablesview-fetched", onNodeListVviewFetched); + + executeSoon(() => + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow) + ); +} + +function onNodeListVviewFetched(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + // Transform the vview into an array we can filter properties from + let props = [[id, prop] for([id, prop] of aVar)]; + // These properties are the DOM nodes ones + props = props.filter(v => v[0].match(/[0-9]+/)); + + function hoverOverDomNodeVariableAndAssertHighlighter(index) { + if (props[index]) { + let prop = props[index][1]; + let valueEl = prop._valueLabel; + + gToolbox.once("node-highlight", () => { + ok(true, "The highlighter was shown on hover of the DOMNode"); + gToolbox.highlighterUtils.unhighlight().then(() => { + clickOnDomNodeVariableAndAssertInspectorSelected(index); + }); + }); + + // Rather than trying to emulate a mouseenter event, let's call the + // variable's highlightDomNode and see if it has the desired effect + prop.highlightDomNode(); + } else { + finishUp(); + } + } + + function clickOnDomNodeVariableAndAssertInspectorSelected(index) { + let prop = props[index][1]; + + // Make sure the inspector is initialized so we can listen to its events + gToolbox.initInspector().then(() => { + // Rather than trying to click on the value here, let's just call the + // variable's openNodeInInspector function and see if it has the + // desired effect + prop.openNodeInInspector().then(() => { + is(gToolbox.currentToolId, "inspector", "The toolbox switched over the inspector on DOMNode click"); + gToolbox.selectTool("webconsole").then(() => { + hoverOverDomNodeVariableAndAssertHighlighter(index + 1); + }); + }); + }); + } + + hoverOverDomNodeVariableAndAssertHighlighter(0); +} + +function finishUp() { + gWebConsole = gJSTerm = gVariablesView = gToolbox = null; + + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js new file mode 100644 index 000000000..0c6f56973 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js @@ -0,0 +1,131 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger, when changing the value of a property in the variables +// view. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, + gStackframes, gVariablesView; + +function test() +{ + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + + executeSoon(() => { + info("openDebugger"); + openDebugger().then(debuggerOpened); + }); +} + +function debuggerOpened(aResult) +{ + gDebuggerWin = aResult.panelWin; + gDebuggerController = gDebuggerWin.DebuggerController; + gThread = gDebuggerController.activeThread; + gStackframes = gDebuggerController.StackFrames; + + executeSoon(() => { + gThread.addOneTimeListener("framesadded", onFramesAdded); + + info("firstCall()"); + content.wrappedJSObject.firstCall(); + }); +} + +function onFramesAdded() +{ + info("onFramesAdded"); + + executeSoon(() => + openConsole().then(() => + gJSTerm.execute("fooObj").then(onExecuteFooObj) + ) + ); +} + + +function onExecuteFooObj(msg) +{ + ok(msg, "output message found"); + ok(msg.textContent.contains('{ testProp2: "testValue2" }'), "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + gJSTerm.once("variablesview-fetched", onFooObjFetch); + + executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {}, + gWebConsole.iframeWindow)); +} + +function onFooObjFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: "testValue2" }, + { name: "testProp", value: "testValue", dontMatch: true }, + ], { webconsole: gWebConsole }).then(onTestPropFound); +} + +function onTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the |testProp2| property in the variables view"); + + // Check that property value updates work and that jsterm functions can be + // used. + updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + foo2 + $('p')", + webconsole: gWebConsole + }).then(onFooObjFetchAfterUpdate); +} + +function onFooObjFetchAfterUpdate(aVar) +{ + info("onFooObjFetchAfterUpdate"); + let para = content.wrappedJSObject.document.querySelector("p"); + let expectedValue = content.document.title + "foo2SecondCall" + para; + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound); +} + +function onUpdatedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the updated |testProp2| property value"); + + // Check that testProp2 was updated. + executeSoon(() => gJSTerm.execute("fooObj.testProp2").then(onExecuteFooObjTestProp2)); +} + +function onExecuteFooObjTestProp2() +{ + let para = content.wrappedJSObject.document.querySelector("p"); + let expected = content.document.title + "foo2SecondCall" + para; + + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "fooObj.testProp2 is correct"); + + gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController = + gStackframes = gVariablesView = null; + executeSoon(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js new file mode 100644 index 000000000..55890c3f7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js @@ -0,0 +1,129 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval works while the js debugger paused the +// page, and while the inspector is active. See bug 886137. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, + gStackframes, gVariablesView; + +function test() +{ + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }, true); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + + info("openDebugger"); + openDebugger().then(debuggerOpened); +} + +function debuggerOpened(aResult) +{ + info("debugger opened"); + gDebuggerWin = aResult.panelWin; + gDebuggerController = gDebuggerWin.DebuggerController; + gThread = gDebuggerController.activeThread; + gStackframes = gDebuggerController.StackFrames; + + openInspector().then(inspectorOpened); +} + +function inspectorOpened(aPanel) +{ + info("inspector opened"); + gThread.addOneTimeListener("framesadded", onFramesAdded); + + info("firstCall()"); + content.wrappedJSObject.firstCall(); +} + +function onFramesAdded() +{ + info("onFramesAdded"); + + openConsole().then(() => gJSTerm.execute("fooObj").then(onExecuteFooObj)); +} + +function onExecuteFooObj(msg) +{ + ok(msg, "output message found"); + ok(msg.textContent.contains('{ testProp2: "testValue2" }'), + "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + gJSTerm.once("variablesview-fetched", onFooObjFetch); + + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow); +} + +function onFooObjFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: "testValue2" }, + { name: "testProp", value: "testValue", dontMatch: true }, + ], { webconsole: gWebConsole }).then(onTestPropFound); +} + +function onTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the |testProp2| property in the variables view"); + + // Check that property value updates work and that jsterm functions can be + // used. + updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + foo2 + $('p')", + webconsole: gWebConsole + }).then(onFooObjFetchAfterUpdate); +} + +function onFooObjFetchAfterUpdate(aVar) +{ + info("onFooObjFetchAfterUpdate"); + let para = content.wrappedJSObject.document.querySelector("p"); + let expectedValue = content.document.title + "foo2SecondCall" + para; + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound); +} + +function onUpdatedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the updated |testProp2| property value"); + + // Check that testProp2 was updated. + gJSTerm.execute("fooObj.testProp2").then(onExecuteFooObjTestProp2); +} + +function onExecuteFooObjTestProp2() +{ + let para = content.wrappedJSObject.document.querySelector("p"); + let expected = content.document.title + "foo2SecondCall" + para; + + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "fooObj.testProp2 is correct"); + + gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController = + gStackframes = gVariablesView = null; + + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js new file mode 100644 index 000000000..beed02552 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js @@ -0,0 +1,147 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, gStackframes; + +function test() +{ + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gJSTerm.execute("foo").then(onExecuteFoo); +} + +function onExecuteFoo() +{ + isnot(gWebConsole.outputNode.textContent.indexOf("globalFooBug783499"), -1, + "|foo| value is correct"); + + gJSTerm.clearOutput(); + + // Test for Bug 690529 - Web Console and Scratchpad should evaluate + // expressions in the scope of the content window, not in a sandbox. + executeSoon(() => gJSTerm.execute("foo2 = 'newFoo'; window.foo2").then(onNewFoo2)); +} + +function onNewFoo2(msg) +{ + is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1, + "|undefined| is not displayed after adding |foo2|"); + + ok(msg, "output result found"); + + isnot(msg.textContent.indexOf("newFoo"), -1, + "'newFoo' is displayed after adding |foo2|"); + + gJSTerm.clearOutput(); + + info("openDebugger"); + executeSoon(() => openDebugger().then(debuggerOpened)); +} + +function debuggerOpened(aResult) +{ + gDebuggerWin = aResult.panelWin; + gDebuggerController = gDebuggerWin.DebuggerController; + gThread = gDebuggerController.activeThread; + gStackframes = gDebuggerController.StackFrames; + + info("openConsole"); + executeSoon(() => + openConsole().then(() => + gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2) + ) + ); +} + +function onExecuteFooAndFoo2() +{ + let expected = "globalFooBug783499newFoo"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2| is displayed after starting the debugger"); + + executeSoon(() => { + gJSTerm.clearOutput(); + + info("openDebugger"); + openDebugger().then(() => { + gThread.addOneTimeListener("framesadded", onFramesAdded); + + info("firstCall()"); + content.wrappedJSObject.firstCall(); + }); + }); +} + +function onFramesAdded() +{ + info("onFramesAdded, openConsole() now"); + executeSoon(() => + openConsole().then(() => + gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2InSecondCall) + ) + ); +} + +function onExecuteFooAndFoo2InSecondCall() +{ + let expected = "globalFooBug783499foo2SecondCall"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2| from |secondCall()|"); + + executeSoon(() => { + gJSTerm.clearOutput(); + + info("openDebugger and selectFrame(1)"); + + openDebugger().then(() => { + gStackframes.selectFrame(1); + + info("openConsole"); + executeSoon(() => + openConsole().then(() => + gJSTerm.execute("foo + foo2 + foo3").then(onExecuteFoo23InFirstCall) + ) + ); + }); + }); +} + +function onExecuteFoo23InFirstCall() +{ + let expected = "fooFirstCallnewFoofoo3FirstCall"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2 + foo3| from |firstCall()|"); + + executeSoon(() => + gJSTerm.execute("foo = 'abba'; foo3 = 'bug783499'; foo + foo3").then( + onExecuteFooAndFoo3ChangesInFirstCall)); +} + +function onExecuteFooAndFoo3ChangesInFirstCall() +{ + let expected = "abbabug783499"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo3| updated in |firstCall()|"); + + is(content.wrappedJSObject.foo, "globalFooBug783499", "|foo| in content window"); + is(content.wrappedJSObject.foo2, "newFoo", "|foo2| in content window"); + ok(!content.wrappedJSObject.foo3, "|foo3| was not added to the content window"); + + gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController = + gStackframes = null; + executeSoon(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe2.js b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe2.js new file mode 100644 index 000000000..e94375181 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe2.js @@ -0,0 +1,63 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test to make sure that web console commands can fire while paused at a breakpoint +// that was triggered from a JS call. Relies on asynchronous js evaluation over the +// protocol - see Bug 1088861. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + + info("open the web console"); + let hud = yield openConsole(); + let {jsterm} = hud; + + info("open the debugger"); + let {panel,panelWin} = yield openDebugger(); + let {DebuggerController} = panelWin; + let {activeThread,StackFrames} = DebuggerController; + + let firstCall = promise.defer(); + let frameAdded = promise.defer(); + executeSoon(() => { + info ("Executing firstCall"); + activeThread.addOneTimeListener("framesadded", () => { + executeSoon(frameAdded.resolve); + }); + jsterm.execute("firstCall()").then(firstCall.resolve); + }); + + info ("Waiting for a frame to be added"); + yield frameAdded.promise; + + info ("Executing basic command while paused"); + yield executeAndConfirm(jsterm, "1 + 2", "3"); + + info ("Executing command using scoped variables while paused"); + yield executeAndConfirm(jsterm, "foo + foo2", '"globalFooBug783499foo2SecondCall"'); + + info ("Resuming the thread"); + activeThread.resume(); + + info ("Checking the first command (which is the last to resolve since it paused"); + let node = yield firstCall.promise; + is (node.querySelector(".message-body").textContent, + "undefined", + "firstCall() returned correct value"); +}); + +function* executeAndConfirm(jsterm, input, output) { + info ("Executing command `"+input+"`"); + + let node = yield jsterm.execute(input); + + is (node.querySelector(".message-body").textContent, + output, + "Expected result from call to " + input); +} + diff --git a/browser/devtools/webconsole/test/browser_jsterm_inspect.js b/browser/devtools/webconsole/test/browser_jsterm_inspect.js new file mode 100644 index 000000000..6bc5f5559 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_jsterm_inspect.js @@ -0,0 +1,28 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that the inspect() jsterm helper function works. + +const TEST_URI = "data:text/html;charset=utf8,<p>hello bug 869981"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + let jsterm = hud.jsterm; + + jsterm.execute("testProp = 'testValue'"); + + let fetched = jsterm.once("variablesview-fetched"); + jsterm.execute("inspect(window)"); + let variable = yield fetched; + + ok(variable._variablesView, "variables view object"); + + yield findVariableViewProperties(variable, [ + { name: "testProp", value: "testValue" }, + { name: "document", value: /HTMLDocument \u2192 data:/ }, + ], { webconsole: hud }); +}); diff --git a/browser/devtools/webconsole/test/browser_longstring_hang.js b/browser/devtools/webconsole/test/browser_longstring_hang.js new file mode 100644 index 000000000..5d8dc04cc --- /dev/null +++ b/browser/devtools/webconsole/test/browser_longstring_hang.js @@ -0,0 +1,53 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that very long strings do not hang the browser. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + info("wait for the initial long string"); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "find 'foobar', no 'foobaz', in long string output", + text: "foobar", + noText: "foobaz", + category: CATEGORY_WEBDEV, + longString: true, + }, + ], + }); + + let clickable = results[0].longStrings[0]; + ok(clickable, "long string ellipsis is shown"); + clickable.scrollIntoView(false); + + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + + info("wait for long string expansion"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "find 'foobaz' after expand, but no 'boom!' at the end", + text: "foobaz", + noText: "boom!", + category: CATEGORY_WEBDEV, + longString: false, + }, + { + text: "too long to be displayed", + longString: false, + }, + ], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js b/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js new file mode 100644 index 000000000..d19c393a7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js @@ -0,0 +1,307 @@ +/* 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 the network panel works with LongStringActors. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; + +const TEST_IMG_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJREFU" + + "OI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuFDjq5dxD8" + + "FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNUyhsKAChRBQcP" + + "FVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQEod+Z5sP1FizDxGgy" + + "BqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTsDizDRMPuIOC+zEeTMZo9" + + "BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joREN2uqnlBmCwW1hIJagtev4f3z" + + "A16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOnidadmwtwsnMu18q83/kHSou+bFND" + + "Dr4AAAAASUVORK5CYII="; + +let testDriver; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(testNetworkPanel); + }); +} + +function testNetworkPanel() { + testDriver = testGen(); + testDriver.next(); +} + +function checkIsVisible(aPanel, aList) { + for (let id in aList) { + let node = aPanel.document.getElementById(id); + let isVisible = aList[id]; + is(node.style.display, (isVisible ? "block" : "none"), id + " isVisible=" + isVisible); + } +} + +function checkNodeContent(aPanel, aId, aContent) { + let node = aPanel.document.getElementById(aId); + if (node == null) { + ok(false, "Tried to access node " + aId + " that doesn't exist!"); + } + else if (node.textContent.indexOf(aContent) != -1) { + ok(true, "checking content of " + aId); + } + else { + ok(false, "Got false value for " + aId + ": " + node.textContent + " doesn't have " + aContent); + } +} + +function checkNodeKeyValue(aPanel, aId, aKey, aValue) { + let node = aPanel.document.getElementById(aId); + + let headers = node.querySelectorAll("th"); + for (let i = 0; i < headers.length; i++) { + if (headers[i].textContent == (aKey + ":")) { + is(headers[i].nextElementSibling.textContent, aValue, + "checking content of " + aId + " for key " + aKey); + return; + } + } + + ok(false, "content check failed for " + aId + ", key " + aKey); +} + +function testGen() { + let hud = HUDService.getHudByWindow(content); + let filterBox = hud.ui.filterBox; + + let headerValue = (new Array(456)).join("fooz bar"); + let headerValueGrip = { + type: "longString", + initial: headerValue.substr(0, 123), + length: headerValue.length, + actor: "faktor", + _fullString: headerValue, + }; + + let imageContentGrip = { + type: "longString", + initial: TEST_IMG_BASE64.substr(0, 143), + length: TEST_IMG_BASE64.length, + actor: "faktor2", + _fullString: TEST_IMG_BASE64, + }; + + let postDataValue = (new Array(123)).join("post me"); + let postDataGrip = { + type: "longString", + initial: postDataValue.substr(0, 172), + length: postDataValue.length, + actor: "faktor3", + _fullString: postDataValue, + }; + + let httpActivity = { + updates: ["responseContent", "eventTimings"], + discardRequestBody: false, + discardResponseBody: false, + startedDateTime: (new Date()).toISOString(), + request: { + url: TEST_IMG, + method: "GET", + cookies: [], + headers: [ + { name: "foo", value: "bar" }, + { name: "loongstring", value: headerValueGrip }, + ], + postData: { text: postDataGrip }, + }, + response: { + httpVersion: "HTTP/3.14", + status: 2012, + statusText: "ddahl likes tacos :)", + headers: [ + { name: "Content-Type", value: "image/png" }, + ], + content: { mimeType: "image/png", text: imageContentGrip }, + cookies: [], + }, + timings: { wait: 15, receive: 23 }, + }; + + let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + + is(filterBox._netPanel, networkPanel, + "Network panel stored on the anchor object"); + + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + info("test 1: check if a header value is expandable"); + + checkIsVisible(networkPanel, { + requestCookie: false, + requestFormData: false, + requestBody: false, + requestBodyFetchLink: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: true, + responseImageCached: false, + responseBodyFetchLink: true, + }); + + checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar"); + checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring", + headerValueGrip.initial + "[\u2026]"); + + let webConsoleClient = networkPanel.webconsole.webConsoleClient; + let longStringFn = webConsoleClient.longString; + + let expectedGrip = headerValueGrip; + + function longStringClientProvider(aLongString) + { + is(aLongString, expectedGrip, + "longString grip is correct"); + + return { + initial: expectedGrip.initial, + length: expectedGrip.length, + substring: function(aStart, aEnd, aCallback) { + is(aStart, expectedGrip.initial.length, + "substring start is correct"); + is(aEnd, expectedGrip.length, + "substring end is correct"); + + executeSoon(function() { + aCallback({ + substring: expectedGrip._fullString.substring(aStart, aEnd), + }); + + executeSoon(function() { + testDriver.next(); + }); + }); + }, + }; + } + + webConsoleClient.longString = longStringClientProvider; + + let clickable = networkPanel.document + .querySelector("#requestHeadersContent .longStringEllipsis"); + ok(clickable, "long string ellipsis is shown"); + + EventUtils.sendMouseEvent({ type: "mousedown"}, clickable, + networkPanel.document.defaultView); + + yield undefined; + + clickable = networkPanel.document + .querySelector("#requestHeadersContent .longStringEllipsis"); + ok(!clickable, "long string ellipsis is not shown"); + + checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring", + expectedGrip._fullString); + + info("test 2: check that response body image fetching works"); + expectedGrip = imageContentGrip; + + let imgNode = networkPanel.document.getElementById("responseImageNode"); + ok(!imgNode.getAttribute("src"), "no image is displayed"); + + clickable = networkPanel.document.querySelector("#responseBodyFetchLink"); + EventUtils.sendMouseEvent({ type: "mousedown"}, clickable, + networkPanel.document.defaultView); + + yield undefined; + + imgNode = networkPanel.document.getElementById("responseImageNode"); + is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64, + "displayed image is correct"); + is(clickable.style.display, "none", "#responseBodyFetchLink is not visible"); + + info("test 3: expand the request body"); + + expectedGrip = postDataGrip; + + clickable = networkPanel.document.querySelector("#requestBodyFetchLink"); + EventUtils.sendMouseEvent({ type: "mousedown"}, clickable, + networkPanel.document.defaultView); + yield undefined; + + is(clickable.style.display, "none", "#requestBodyFetchLink is not visible"); + + checkIsVisible(networkPanel, { + requestBody: true, + requestBodyFetchLink: false, + }); + + checkNodeContent(networkPanel, "requestBodyContent", expectedGrip._fullString); + + webConsoleClient.longString = longStringFn; + + networkPanel.panel.hidePopup(); + + info("test 4: reponse body long text"); + + httpActivity.response.content.mimeType = "text/plain"; + httpActivity.response.headers[0].value = "text/plain"; + + expectedGrip = imageContentGrip; + + // Reset response.content.text to avoid caching of the full string. + httpActivity.response.content.text = expectedGrip; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + is(filterBox._netPanel, networkPanel, + "Network panel stored on httpActivity object"); + + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestCookie: false, + requestFormData: false, + requestBody: true, + requestBodyFetchLink: false, + responseContainer: true, + responseBody: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false, + responseBodyFetchLink: true, + }); + + checkNodeContent(networkPanel, "responseBodyContent", expectedGrip.initial); + + webConsoleClient.longString = longStringClientProvider; + + clickable = networkPanel.document.querySelector("#responseBodyFetchLink"); + EventUtils.sendMouseEvent({ type: "mousedown"}, clickable, + networkPanel.document.defaultView); + + yield undefined; + + webConsoleClient.longString = longStringFn; + is(clickable.style.display, "none", "#responseBodyFetchLink is not visible"); + checkNodeContent(networkPanel, "responseBodyContent", expectedGrip._fullString); + + networkPanel.panel.hidePopup(); + + // All done! + testDriver = null; + executeSoon(finishTest); + + yield undefined; +} diff --git a/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js new file mode 100644 index 000000000..b7e86b94c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js @@ -0,0 +1,44 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Make sure that the Web Console output does not break after we try to call +// console.dir() for objects that are not inspectable. + +const TEST_URI = "data:text/html;charset=utf8,test for bug 773466"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(true); + + hud.jsterm.execute("console.log('fooBug773466a')"); + hud.jsterm.execute("myObj = Object.create(null)"); + hud.jsterm.execute("console.dir(myObj)"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooBug773466a", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "console.dir output", + consoleDir: "[object Object]", + }], + }) + + content.console.log("fooBug773466b"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooBug773466b", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_output_longstring_expand.js b/browser/devtools/webconsole/test/browser_output_longstring_expand.js new file mode 100644 index 000000000..7d5d785c3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_output_longstring_expand.js @@ -0,0 +1,83 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that long strings can be expanded in the console output. + +const TEST_URI = "data:text/html;charset=utf8,test for bug 787981 - check that long strings can be expanded in the output."; + +let test = asyncTest(function* () { + let tempScope = {}; + Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope); + let DebuggerServer = tempScope.DebuggerServer; + + let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a") + + "foobar"; + let initialString = + longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(true); + hud.jsterm.execute("console.log('bazbaz', '" + longString +"', 'boom')"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log output", + text: ["bazbaz", "boom", initialString], + noText: "foobar", + longString: true, + }], + }); + + let clickable = result.longStrings[0]; + ok(clickable, "long string ellipsis is shown"); + + clickable.scrollIntoView(false); + + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "full string", + text: ["bazbaz", "boom", longString], + category: CATEGORY_WEBDEV, + longString: false, + }], + }); + + hud.jsterm.clearOutput(true); + let msg = yield execute(hud, "'" + longString +"'"); + + isnot(msg.textContent.indexOf(initialString), -1, + "initial string is shown"); + is(msg.textContent.indexOf(longString), -1, + "full string is not shown"); + + clickable = msg.querySelector(".longStringEllipsis"); + ok(clickable, "long string ellipsis is shown"); + + clickable.scrollIntoView(false); + + EventUtils.synthesizeMouse(clickable, 3, 4, {}, hud.iframeWindow); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "full string", + text: longString, + category: CATEGORY_OUTPUT, + longString: false, + }], + }) +}); + +function execute(hud, str) { + let deferred = promise.defer(); + hud.jsterm.execute(str, deferred.resolve); + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js b/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js new file mode 100644 index 000000000..bd092bbc2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_repeated_messages_accuracy.js @@ -0,0 +1,125 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure messages are not considered repeated when coming from +// different lines of code, or from different severities, etc. +// See bugs 720180 and 800510. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-repeated-messages.html"; +const PREF = "devtools.webconsole.persistlog"; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref(PREF, true); + + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield consoleOpened(hud); + + let loaded = loadBrowser(browser); + BrowserReload(); + yield loaded; + + yield testCSSRepeats(hud); + yield testCSSRepeatsAfterReload(hud); + yield testConsoleRepeats(hud); + + Services.prefs.clearUserPref(PREF); +}); + +function consoleOpened(hud) { + // Check that css warnings are not coalesced if they come from different lines. + info("waiting for 2 css warnings"); + + return waitForMessages({ + webconsole: hud, + messages: [{ + name: "two css warnings", + category: CATEGORY_CSS, + count: 2, + repeats: 1, + }], + }); +} + +function testCSSRepeats(hud) { + info("wait for repeats after page reload"); + + return waitForMessages({ + webconsole: hud, + messages: [{ + name: "two css warnings, repeated twice", + category: CATEGORY_CSS, + repeats: 2, + count: 2, + }], + }); +} + +function testCSSRepeatsAfterReload(hud) { + hud.jsterm.clearOutput(true); + hud.jsterm.execute("testConsole()"); + + info("wait for repeats with the console API"); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + name: "console.log 'foo repeat' repeated twice", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2, + }, + { + name: "console.log 'foo repeat' repeated once", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 1, + }, + { + name: "console.error 'foo repeat' repeated once", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + repeats: 1, + }, + ], + }) +} + +function testConsoleRepeats(hud) { + hud.jsterm.clearOutput(true); + hud.jsterm.execute("undefined"); + + content.console.log("undefined"); + + info("make sure console API messages are not coalesced with jsterm output"); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + name: "'undefined' jsterm input message", + text: "undefined", + category: CATEGORY_INPUT, + }, + { + name: "'undefined' jsterm output message", + text: "undefined", + category: CATEGORY_OUTPUT, + }, + { + name: "'undefined' console.log message", + text: "undefined", + category: CATEGORY_WEBDEV, + repeats: 1, + }, + ], + }); +} diff --git a/browser/devtools/webconsole/test/browser_result_format_as_string.js b/browser/devtools/webconsole/test/browser_result_format_as_string.js new file mode 100644 index 000000000..70c3cc61a --- /dev/null +++ b/browser/devtools/webconsole/test/browser_result_format_as_string.js @@ -0,0 +1,43 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed"); + +// Make sure that JS eval result are properly formatted as strings. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-result-format-as-string.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(true); + + let msg = yield execute(hud, "document.querySelector('p')"); + + is(hud.outputNode.textContent.indexOf("bug772506_content"), -1, + "no content element found"); + ok(!hud.outputNode.querySelector("#foobar"), "no #foobar element found"); + + ok(msg, "eval output node found"); + is(msg.textContent.indexOf("<div>"), -1, + "<div> string is not displayed"); + isnot(msg.textContent.indexOf("<p>"), -1, + "<p> string is displayed"); + + EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"}); + ok(!gBrowser._bug772506, "no content variable"); +}); + +function execute(hud, str) { + let deferred = promise.defer(); + hud.jsterm.execute(str, deferred.resolve); + return deferred.promise; +}
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js b/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js new file mode 100644 index 000000000..adef430ad --- /dev/null +++ b/browser/devtools/webconsole/test/browser_warn_user_about_replaced_api.js @@ -0,0 +1,81 @@ +/* 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/. */ + +const TEST_REPLACED_API_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-replaced-api.html"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/testscript.js"; +const PREF = "devtools.webconsole.persistlog"; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref(PREF, true); + + let { browser } = yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield testWarningNotPresent(hud); + + let loaded = loadBrowser(browser); + content.location = TEST_REPLACED_API_URI; + yield loaded; + + let hud2 = yield openConsole(); + + yield testWarningPresent(hud2); + + Services.prefs.clearUserPref(PREF); +}); + +function testWarningNotPresent(hud) +{ + let deferred = promise.defer(); + + is(hud.outputNode.textContent.indexOf("logging API"), -1, + "no warning displayed"); + + // Bug 862024: make sure the warning doesn't show after page reload. + info("reload " + TEST_URI); + executeSoon(() => content.location.reload()); + + waitForMessages({ + webconsole: hud, + messages: [{ + text: "testscript.js", + category: CATEGORY_NETWORK, + }], + }).then(() => executeSoon(() => { + is(hud.outputNode.textContent.indexOf("logging API"), -1, + "no warning displayed"); + closeConsole().then(deferred.resolve); + })); + + return deferred.promise; +} + +function testWarningPresent(hud) +{ + info("wait for the warning to show"); + let deferred = promise.defer(); + + let warning = { + webconsole: hud, + messages: [{ + text: /logging API .+ disabled by a script/, + category: CATEGORY_JS, + severity: SEVERITY_WARNING, + }], + }; + + waitForMessages(warning).then(() => { + hud.jsterm.clearOutput(); + + executeSoon(() => { + info("reload the test page and wait for the warning to show"); + waitForMessages(warning).then(deferred.resolve); + content.location.reload(); + }); + }); + + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js b/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js new file mode 100644 index 000000000..1bc94b5d3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that source URLs are abbreviated properly for display on the right- +// hand side of the Web Console. + +function test() { + testAbbreviation("http://example.com/x.js", "x.js"); + testAbbreviation("http://example.com/foo/bar/baz/boo.js", "boo.js"); + testAbbreviation("http://example.com/foo/bar/", "bar"); + testAbbreviation("http://example.com/foo.js?bar=1&baz=2", "foo.js"); + testAbbreviation("http://example.com/foo/?bar=1&baz=2", "foo"); + + finishTest(); +} + +function testAbbreviation(aFullURL, aAbbreviatedURL) { + is(WebConsoleUtils.abbreviateSourceURL(aFullURL), aAbbreviatedURL, aFullURL + + " is abbreviated to " + aAbbreviatedURL); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js b/browser/devtools/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js new file mode 100644 index 000000000..4913f5ccf --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The test loads a web page with mixed active and display content +// on it while the "block mixed content" settings are _off_. +// It then checks that the loading mixed content warning messages +// are logged to the console and have the correct "Learn More" +// url appended to them. +// Bug 875456 - Log mixed content messages from the Mixed Content +// Blocker to the Security Pane in the Web Console + +const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/MixedContent"; + +let test = asyncTest(function* () { + yield pushPrefEnv(); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Logged mixed active content", + text: "Loading mixed (insecure) active content \"http://example.com/\" on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + { + name: "Logged mixed passive content - image", + text: "Loading mixed (insecure) display content \"http://example.com/tests/image/test/mochitest/blue.png\" on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + ], + }); + + yield testClickOpenNewTab(hud, results); +}); + +function pushPrefEnv() +{ + let deferred = promise.defer(); + let options = {"set": + [["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false] + ]}; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} + +function testClickOpenNewTab(hud, results) { + let warningNode = results[0].clickableElements[0]; + ok(warningNode, "link element"); + ok(warningNode.classList.contains("learn-more-link"), "link class name"); + return simulateMessageLinkClick(warningNode, LEARN_MORE_URI); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_assert.js b/browser/devtools/webconsole/test/browser_webconsole_assert.js new file mode 100644 index 000000000..10500a7a8 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_assert.js @@ -0,0 +1,51 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that console.assert() works as expected (i.e. outputs only on falsy +// asserts). See bug 760193. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-assert.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield consoleOpened(hud); +}); + +function consoleOpened(hud) { + hud.jsterm.execute("test()"); + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "undefined", + category: CATEGORY_OUTPUT, + severity: SEVERITY_LOG, + }, + { + text: "start", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "false assert", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + }, + { + text: "falsy assert", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + }, + { + text: "end", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + let nodes = hud.outputNode.querySelectorAll(".message"); + is(nodes.length, 6, "only six messages are displayed, no output from the true assert"); + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js b/browser/devtools/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js new file mode 100644 index 000000000..0577b8cc2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js @@ -0,0 +1,40 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +// Test that properties starting with underscores or dollars can be +// autocompleted (bug 967468). + + +let test = asyncTest(function*() { + const TEST_URI = "data:text/html;charset=utf8,test autocompletion with $ or _"; + yield loadTab(TEST_URI); + + function autocomplete(term) { + let deferred = promise.defer(); + + jsterm.setInputValue(term); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, deferred.resolve); + + yield deferred.promise; + + ok(popup.itemCount > 0, "There's suggestions for '" + term + "'"); + } + + let { jsterm } = yield openConsole(); + let popup = jsterm.autocompletePopup; + + yield jsterm.execute("let testObject = {$$aaab: '', $$aaac: ''}"); + + // Should work with bug 967468. + yield autocomplete("Object.__d"); + yield autocomplete("testObject.$$a"); + + // Here's when things go wrong in bug 967468. + yield autocomplete("Object.__de"); + yield autocomplete("testObject.$$aa"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js new file mode 100644 index 000000000..69ccb443e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js @@ -0,0 +1,127 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642615"; + +XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield consoleOpened(hud); +}); + +function consoleOpened(HUD) { + let deferred = promise.defer(); + + let jsterm = HUD.jsterm; + let stringToCopy = "foobazbarBug642615"; + + jsterm.clearOutput(); + + ok(!jsterm.completeNode.value, "no completeNode.value"); + + jsterm.setInputValue("doc"); + + let completionValue; + + // wait for key "u" + function onCompletionValue() { + completionValue = jsterm.completeNode.value; + + // Arguments: expected, setup, success, failure. + waitForClipboard( + stringToCopy, + function() { + clipboardHelper.copyString(stringToCopy, document); + }, + onClipboardCopy, + finishTest); + } + + function onClipboardCopy() { + testSelfXss(); + + jsterm.setInputValue("docu"); + info("wait for completion update after clipboard paste"); + updateEditUIVisibility(); + jsterm.once("autocomplete-updated", onClipboardPaste); + goDoCommand("cmd_paste"); + } + + + // Self xss prevention tests (bug 994134) + function testSelfXss(){ + info("Self-xss paste tests") + WebConsoleUtils.usageCount = 0; + is(WebConsoleUtils.usageCount, 0, "Test for usage count getter") + // Input some commands to check if usage counting is working + for(let i = 0; i <= 3; i++){ + jsterm.setInputValue(i); + jsterm.execute(); + } + is(WebConsoleUtils.usageCount, 4, "Usage count incremented") + WebConsoleUtils.usageCount = 0; + updateEditUIVisibility(); + + let oldVal = jsterm.inputNode.value; + goDoCommand("cmd_paste"); + let notificationbox = jsterm.hud.document.getElementById("webconsole-notificationbox"); + let notification = notificationbox.getNotificationWithValue('selfxss-notification'); + ok(notification, "Self-xss notification shown"); + is(oldVal, jsterm.inputNode.value, "Paste blocked by self-xss prevention"); + + // Allow pasting + jsterm.inputNode.value = "allow pasting"; + var evt = document.createEvent("KeyboardEvent"); + evt.initKeyEvent ("keyup", true, true, window, + 0, 0, 0, 0, + 0, " ".charCodeAt(0)); + jsterm.inputNode.dispatchEvent(evt); + jsterm.inputNode.value = ""; + goDoCommand("cmd_paste"); + isnot("", jsterm.inputNode.value, "Paste works"); + } + function onClipboardPaste() { + ok(!jsterm.completeNode.value, "no completion value after paste"); + + info("wait for completion update after undo"); + jsterm.once("autocomplete-updated", onCompletionValueAfterUndo); + + // Get out of the webconsole event loop. + executeSoon(() => { + goDoCommand("cmd_undo"); + }); + } + + function onCompletionValueAfterUndo() { + is(jsterm.completeNode.value, completionValue, + "same completeNode.value after undo"); + + info("wait for completion update after clipboard paste (ctrl-v)"); + jsterm.once("autocomplete-updated", () => { + ok(!jsterm.completeNode.value, "no completion value after paste (ctrl-v)"); + + // using executeSoon() to get out of the webconsole event loop. + executeSoon(deferred.resolve); + }); + + // Get out of the webconsole event loop. + executeSoon(() => { + EventUtils.synthesizeKey("v", {accelKey: true}); + }); + } + + info("wait for completion value after typing 'docu'"); + jsterm.once("autocomplete-updated", onCompletionValue); + + EventUtils.synthesizeKey("u", {}); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js new file mode 100644 index 000000000..b5e603164 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that autocomplete doesn't break when trying to reach into objects from +// a different domain, bug 989025. + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-989025-iframe-parent.html"; + + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + + hud.jsterm.execute('document.title'); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "989025 - iframe parent", + category: CATEGORY_OUTPUT, + }], + }); + + let autocompleteUpdated = hud.jsterm.once("autocomplete-updated"); + + hud.jsterm.setInputValue("window[0].document"); + executeSoon(() => { + EventUtils.synthesizeKey(".", {}); + }); + + yield autocompleteUpdated; + + hud.jsterm.setInputValue("window[0].document.title"); + EventUtils.synthesizeKey("VK_RETURN", {}); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Permission denied", + category: CATEGORY_OUTPUT, + severity: SEVERITY_ERROR, + }], + }); + + hud.jsterm.execute("window.location"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-bug-989025-iframe-parent.html", + category: CATEGORY_OUTPUT, + }], + }); + + yield closeConsole(tab); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js new file mode 100644 index 000000000..c3c838cb1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js @@ -0,0 +1,242 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console autocomplete happens in the user-selected stackframe +// from the js debugger. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-autocomplete-in-stackframe.html"; + +let testDriver, gStackframes; + +function test() +{ + requestLongerTimeout(2); + loadTab(TEST_URI).then(() => { + openConsole().then((hud) => { + testDriver = testCompletion(hud); + testDriver.next(); + }); + }); +} + +function testNext() { + executeSoon(function() { + testDriver.next(); + }); +} + +function testCompletion(hud) { + let jsterm = hud.jsterm; + let input = jsterm.inputNode; + let popup = jsterm.autocompletePopup; + + // Test that document.title gives string methods. Native getters must execute. + input.value = "document.title."; + input.setSelectionRange(input.value.length, input.value.length); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + let newItems = popup.getItems(); + ok(newItems.length > 0, "'document.title.' gave a list of suggestions"); + ok(newItems.some(function(item) { + return item.label == "substr"; + }), "autocomplete results do contain substr"); + ok(newItems.some(function(item) { + return item.label == "toLowerCase"; + }), "autocomplete results do contain toLowerCase"); + ok(newItems.some(function(item) { + return item.label == "strike"; + }), "autocomplete results do contain strike"); + + // Test if 'f' gives 'foo1' but not 'foo2' or 'foo3' + input.value = "f"; + input.setSelectionRange(1, 1); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(newItems.length > 0, "'f' gave a list of suggestions"); + ok(!newItems.every(function(item) { + return item.label != "foo1"; + }), "autocomplete results do contain foo1"); + ok(!newItems.every(function(item) { + return item.label != "foo1Obj"; + }), "autocomplete results do contain foo1Obj"); + ok(newItems.every(function(item) { + return item.label != "foo2"; + }), "autocomplete results do not contain foo2"); + ok(newItems.every(function(item) { + return item.label != "foo2Obj"; + }), "autocomplete results do not contain foo2Obj"); + ok(newItems.every(function(item) { + return item.label != "foo3"; + }), "autocomplete results do not contain foo3"); + ok(newItems.every(function(item) { + return item.label != "foo3Obj"; + }), "autocomplete results do not contain foo3Obj"); + + // Test if 'foo1Obj.' gives 'prop1' and 'prop2' + input.value = "foo1Obj."; + input.setSelectionRange(8, 8); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "prop1"; + }), "autocomplete results do contain prop1"); + ok(!newItems.every(function(item) { + return item.label != "prop2"; + }), "autocomplete results do contain prop2"); + + // Test if 'foo1Obj.prop2.' gives 'prop21' + input.value = "foo1Obj.prop2."; + input.setSelectionRange(14, 14); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "prop21"; + }), "autocomplete results do contain prop21"); + + info("openDebugger"); + executeSoon(() => openDebugger().then(debuggerOpened)); + yield undefined; + + // From this point on the + // Test if 'f' gives 'foo3' and 'foo1' but not 'foo2' + input.value = "f"; + input.setSelectionRange(1, 1); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(newItems.length > 0, "'f' gave a list of suggestions"); + ok(!newItems.every(function(item) { + return item.label != "foo3"; + }), "autocomplete results do contain foo3"); + ok(!newItems.every(function(item) { + return item.label != "foo3Obj"; + }), "autocomplete results do contain foo3Obj"); + ok(!newItems.every(function(item) { + return item.label != "foo1"; + }), "autocomplete results do contain foo1"); + ok(!newItems.every(function(item) { + return item.label != "foo1Obj"; + }), "autocomplete results do contain foo1Obj"); + ok(newItems.every(function(item) { + return item.label != "foo2"; + }), "autocomplete results do not contain foo2"); + ok(newItems.every(function(item) { + return item.label != "foo2Obj"; + }), "autocomplete results do not contain foo2Obj"); + + openDebugger().then(() => { + gStackframes.selectFrame(1); + + info("openConsole"); + executeSoon(() => openConsole().then(() => testDriver.next())); + }); + yield undefined; + + // Test if 'f' gives 'foo2' and 'foo1' but not 'foo3' + input.value = "f"; + input.setSelectionRange(1, 1); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(newItems.length > 0, "'f' gave a list of suggestions"); + ok(!newItems.every(function(item) { + return item.label != "foo2"; + }), "autocomplete results do contain foo2"); + ok(!newItems.every(function(item) { + return item.label != "foo2Obj"; + }), "autocomplete results do contain foo2Obj"); + ok(!newItems.every(function(item) { + return item.label != "foo1"; + }), "autocomplete results do contain foo1"); + ok(!newItems.every(function(item) { + return item.label != "foo1Obj"; + }), "autocomplete results do contain foo1Obj"); + ok(newItems.every(function(item) { + return item.label != "foo3"; + }), "autocomplete results do not contain foo3"); + ok(newItems.every(function(item) { + return item.label != "foo3Obj"; + }), "autocomplete results do not contain foo3Obj"); + + // Test if 'foo2Obj.' gives 'prop1' + input.value = "foo2Obj."; + input.setSelectionRange(8, 8); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "prop1"; + }), "autocomplete results do contain prop1"); + + // Test if 'foo2Obj.prop1.' gives 'prop11' + input.value = "foo2Obj.prop1."; + input.setSelectionRange(14, 14); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "prop11"; + }), "autocomplete results do contain prop11"); + + // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string i.e. 'length' + input.value = "foo2Obj.prop1.prop11."; + input.setSelectionRange(21, 21); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "length"; + }), "autocomplete results do contain length"); + + // Test if 'foo1Obj[0].' throws no errors. + input.value = "foo2Obj[0]."; + input.setSelectionRange(11, 11); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + is(newItems.length, 0, "no items for foo2Obj[0]"); + + testDriver = null; + executeSoon(finishUp); + yield undefined; +} + +function debuggerOpened(aResult) +{ + let debuggerWin = aResult.panelWin; + let debuggerController = debuggerWin.DebuggerController; + let thread = debuggerController.activeThread; + gStackframes = debuggerController.StackFrames; + + executeSoon(() => { + thread.addOneTimeListener("framesadded", onFramesAdded); + info("firstCall()"); + content.wrappedJSObject.firstCall(); + }); +} + +function onFramesAdded() +{ + info("onFramesAdded, openConsole() now"); + executeSoon(() => openConsole().then(testNext)); +} + +function finishUp() { + testDriver = gStackframes = null; + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js new file mode 100644 index 000000000..19c3ceb49 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js @@ -0,0 +1,33 @@ +/* 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/. */ + +// Test that the autocomplete popup closes on switching tabs. See bug 900448. + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 900448 - autocomplete popup closes on tab switch"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + let popup = hud.jsterm.autocompletePopup; + let popupShown = onPopupShown(popup._panel); + + hud.jsterm.setInputValue("sc"); + EventUtils.synthesizeKey("r", {}); + + yield popupShown; + + ok(!popup.isOpen, "Popup closes on tab switch"); +}); + +function onPopupShown(panel) { + let finished = promise.defer(); + + panel.addEventListener("popupshown", function popupOpened() { + panel.removeEventListener("popupshown", popupOpened, false); + loadTab("data:text/html;charset=utf-8,<p>testing autocomplete closes").then(finished.resolve); + }, false); + + return finished.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js new file mode 100644 index 000000000..c3c8005d4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js @@ -0,0 +1,42 @@ +/* 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 the page's resources are displayed in the console as they're +// loaded + +"use strict"; + +const TEST_NETWORK_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network.html" + "?_date=" + Date.now(); + +let test = asyncTest(function* () { + yield loadTab("data:text/html;charset=utf-8,Web Console basic network logging test"); + let hud = yield openConsole(); + + content.location = TEST_NETWORK_URI; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "running network console", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "test-network.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "testscript.js", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "test-image.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js b/browser/devtools/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js new file mode 100644 index 000000000..47e5cc549 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The test loads a web page with mixed active and display content +// on it while the "block mixed content" settings are _on_. +// It then checks that the blocked mixed content warning messages +// are logged to the console and have the correct "Learn More" +// url appended to them. After the first test finishes, it invokes +// a second test that overrides the mixed content blocker settings +// by clicking on the doorhanger shield and validates that the +// appropriate messages are logged to console. +// Bug 875456 - Log mixed content messages from the Mixed Content +// Blocker to the Security Pane in the Web Console + +const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/MixedContent"; + + +let test = asyncTest(function* () { + yield pushPrefEnv(); + + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Logged blocking mixed active content", + text: "Blocked loading mixed active content \"http://example.com/\"", + category: CATEGORY_SECURITY, + severity: SEVERITY_ERROR, + objects: true, + }, + { + name: "Logged blocking mixed passive content - image", + text: "Blocked loading mixed active content \"http://example.com/\"", + category: CATEGORY_SECURITY, + severity: SEVERITY_ERROR, + objects: true, + }, + ], + }); + + yield testClickOpenNewTab(hud, results[0]); + + let results2 = yield mixedContentOverrideTest2(hud, browser); + + yield testClickOpenNewTab(hud, results2[0]); +}); + +function pushPrefEnv() +{ + let deferred = promise.defer(); + let options = {"set": [["security.mixed_content.block_active_content", true], + ["security.mixed_content.block_display_content", true]]}; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} + +function waitForNotificationShown(notification, callback) +{ + if (PopupNotifications.panel.state == "open") { + executeSoon(callback); + return; + } + PopupNotifications.panel.addEventListener("popupshown", function onShown(e) { + PopupNotifications.panel.removeEventListener("popupshown", onShown); + callback(); + }, false); + notification.reshow(); +} + +function mixedContentOverrideTest2(hud, browser) +{ + var notification = PopupNotifications.getNotification("bad-content", browser); + ok(notification, "Mixed Content Doorhanger did appear"); + let deferred = promise.defer(); + waitForNotificationShown(notification, () => { + afterNotificationShown(hud, notification, deferred); + }); + return deferred.promise; +} + +function afterNotificationShown(hud, notification, deferred) +{ + ok(PopupNotifications.panel.firstChild.isMixedContentBlocked, "OK: Mixed Content is being blocked"); + // Click on the doorhanger. + PopupNotifications.panel.firstChild.disableMixedContentProtection(); + notification.remove(); + + waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Logged blocking mixed active content", + text: "Loading mixed (insecure) active content \"http://example.com/\"" + + " on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + { + name: "Logged blocking mixed passive content - image", + text: "Loading mixed (insecure) display content" + + " \"http://example.com/tests/image/test/mochitest/blue.png\"" + + " on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + ], + }).then(msgs => deferred.resolve(msgs), Cu.reportError); +} + +function testClickOpenNewTab(hud, match) { + let warningNode = match.clickableElements[0]; + ok(warningNode, "link element"); + ok(warningNode.classList.contains("learn-more-link"), "link class name"); + return simulateMessageLinkClick(warningNode, LEARN_MORE_URI); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js b/browser/devtools/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js new file mode 100644 index 000000000..6dd85e939 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js @@ -0,0 +1,41 @@ +/* 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"; + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab("data:text/html;charset=utf8,<title>Test for Bug 1006027"); + + const target = TargetFactory.forTab(tab); + const hud = yield openConsole(tab); + + hud.jsterm.execute("console.log('bug1006027')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log", + text: "bug1006027", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + info('hud.outputNode.textContent:\n'+hud.outputNode.textContent); + let timestampNodes = hud.outputNode.querySelectorAll('span.timestamp'); + let aTimestampMilliseconds = Array.prototype.map.call(timestampNodes, + function (value) { + // We are parsing timestamps as local time, relative to the begin of the epoch. + // This is not the correct value of the timestamp, but good enough for comparison. + return Date.parse('T'+String.trim(value.textContent)); + }); + + let minTimestamp = Math.min.apply(null, aTimestampMilliseconds); + let maxTimestamp = Math.max.apply(null, aTimestampMilliseconds); + ok(Math.abs(maxTimestamp - minTimestamp) < 1000, "console.log message timestamp spread < 1000ms confirmed"); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_1010953_cspro.js b/browser/devtools/webconsole/test/browser_webconsole_bug_1010953_cspro.js new file mode 100644 index 000000000..078f44b74 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_1010953_cspro.js @@ -0,0 +1,47 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * ***** END LICENSE BLOCK ***** */ + +/* We are loading: +a script that is allowed by the CSP header but not by the CSPRO header +an image which is allowed by the CSPRO header but not by the CSP header. + +So we expect a warning (image has been blocked) and a report + (script should not load and was reported) + +The expected console messages in the constants CSP_VIOLATION_MSG and CSP_REPORT_MSG are confirmed to be found in the console messages. +*/ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console CSP report only test (bug 1010953)"; +const TEST_VIOLATION = "http://example.com/browser/browser/devtools/webconsole/test/test_bug_1010953_cspro.html"; +const CSP_VIOLATION_MSG = 'Content Security Policy: The page\'s settings blocked the loading of a resource at http://some.example.com/test.png ("img-src http://example.com").'; +const CSP_REPORT_MSG = 'Content Security Policy: The page\'s settings observed the loading of a resource at http://some.example.com/test_bug_1010953_cspro.js ("script-src http://example.com"). A CSP report is being sent.'; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + content.location = TEST_VIOLATION; + yield loaded; + + let aOutputNode = hud.outputNode; + + yield waitForSuccess({ + name: "Confirmed that CSP and CSP-Report-Only log different messages to the console.", + validator: function() { + console.log(hud.outputNode.textContent); + let success = false; + success = hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1 && + hud.outputNode.textContent.indexOf(CSP_REPORT_MSG) > -1; + return success; + } + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js b/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js new file mode 100644 index 000000000..f396259a5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_578437_page_reload.js @@ -0,0 +1,39 @@ +/* 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 the console object still exists after a page reload. +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let browser; + +function test() { + loadTab(TEST_URI).then({ + openConsole().then((tab) => { + browser = tab.browser; + + browser.addEventListener("DOMContentLoaded", testPageReload, false); + content.location.reload(); + }); + }); + browser.addEventListener("DOMContentLoaded", onLoad, false); +} + +function testPageReload() { + + browser.removeEventListener("DOMContentLoaded", testPageReload, false); + + let console = browser.contentWindow.wrappedJSObject.console; + + is(typeof console, "object", "window.console is an object, after page reload"); + is(typeof console.log, "function", "console.log is a function"); + is(typeof console.info, "function", "console.info is a function"); + is(typeof console.warn, "function", "console.warn is a function"); + is(typeof console.error, "function", "console.error is a function"); + is(typeof console.exception, "function", "console.exception is a function"); + + browser = null; + finishTest(); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js b/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js new file mode 100644 index 000000000..e726a0595 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_579412_input_focus.js @@ -0,0 +1,19 @@ +/* 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 the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let inputNode = hud.jsterm.inputNode; + ok(inputNode.getAttribute("focused"), "input node is focused"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js new file mode 100644 index 000000000..b71d83bd4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js @@ -0,0 +1,48 @@ +/* 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 to ensure that errors don't appear when the console is closed while a +// completion is being performed. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testClosingAfterCompletion(hud, browser); +}); + +function testClosingAfterCompletion(hud, browser) { + let deferred = promise.defer(); + + let inputNode = hud.jsterm.inputNode; + + let errorWhileClosing = false; + function errorListener(evt) { + errorWhileClosing = true; + } + + browser.addEventListener("error", errorListener, false); + + // Focus the inputNode and perform the keycombo to close the WebConsole. + inputNode.focus(); + + gDevTools.once("toolbox-destroyed", function() { + browser.removeEventListener("error", errorListener, false); + is(errorWhileClosing, false, "no error while closing the WebConsole"); + deferred.resolve(); + }); + + if (Services.appinfo.OS == "Darwin") { + EventUtils.synthesizeKey("i", { accelKey: true, altKey: true }); + } else { + EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true }); + } + + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js new file mode 100644 index 000000000..de208ee06 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js @@ -0,0 +1,42 @@ +/* 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 errors still show up in the Web Console after a page reload. +// See bug 580030: the error handler fails silently after page reload. +// https://bugzilla.mozilla.org/show_bug.cgi?id=580030 + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html"; + +function test() { + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + info("console opened"); + + executeSoon(() => { + hud.jsterm.clearOutput(); + info("wait for reload"); + content.location.reload(); + }); + + yield hud.target.once("navigate"); + info("target navigated"); + + let button = content.document.querySelector("button"); + ok(button, "button found"); + + expectUncaughtException(); + EventUtils.sendMouseEvent({type: "click"}, button, content); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooBazBaz is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js new file mode 100644 index 000000000..b2887ddae --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js @@ -0,0 +1,30 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that appropriately-localized timestamps are printed. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + const TEST_TIMESTAMP = 12345678; + let date = new Date(TEST_TIMESTAMP); + let localizedString = WCU_l10n.timestampString(TEST_TIMESTAMP); + isnot(localizedString.indexOf(date.getHours()), -1, "the localized " + + "timestamp contains the hours"); + isnot(localizedString.indexOf(date.getMinutes()), -1, "the localized " + + "timestamp contains the minutes"); + isnot(localizedString.indexOf(date.getSeconds()), -1, "the localized " + + "timestamp contains the seconds"); + isnot(localizedString.indexOf(date.getMilliseconds()), -1, "the localized " + + "timestamp contains the milliseconds"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js new file mode 100644 index 000000000..6032cb761 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js @@ -0,0 +1,44 @@ +/* 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 exceptions thrown by content don't show up twice in the Web +// Console. + +"use strict"; + +const INIT_URI = "data:text/html;charset=utf8,hello world"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-duplicate-error.html"; + +let test = asyncTest(function* () { + yield loadTab(INIT_URI); + + let hud = yield openConsole(); + + expectUncaughtException(); + + content.location = TEST_URI; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooDuplicateError1", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "test-duplicate-error.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); + + let text = hud.outputNode.textContent; + let error1pos = text.indexOf("fooDuplicateError1"); + ok(error1pos > -1, "found fooDuplicateError1"); + if (error1pos > -1) { + ok(text.indexOf("fooDuplicateError1", error1pos + 1) == -1, + "no duplicate for fooDuplicateError1"); + } +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js b/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js new file mode 100644 index 000000000..1f089ad2e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js @@ -0,0 +1,31 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/browser/test-console.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + testCompletion(hud); +}); + +function testCompletion(hud) { + var jsterm = hud.jsterm; + var input = jsterm.inputNode; + + jsterm.setInputValue(""); + EventUtils.synthesizeKey("VK_TAB", {}); + is(jsterm.completeNode.value, "<- no result", "<- no result - matched"); + is(input.value, "", "inputnode is empty - matched") + is(input.getAttribute("focused"), "true", "input is still focused"); + + //Any thing which is not in property autocompleter + jsterm.setInputValue("window.Bug583816"); + EventUtils.synthesizeKey("VK_TAB", {}); + is(jsterm.completeNode.value, " <- no result", "completenode content - matched"); + is(input.value, "window.Bug583816", "inputnode content - matched"); + is(input.getAttribute("focused"), "true", "input is still focused"); +} 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 new file mode 100644 index 000000000..9ec4291e3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js @@ -0,0 +1,87 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that the Web Console limits the number of lines displayed according to +// the user's preferences. + +const TEST_URI = "data:text/html;charset=utf8,test for bug 585237"; + +let outputNode; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let console = content.console; + outputNode = hud.outputNode; + + hud.jsterm.clearOutput(); + + let prefBranch = Services.prefs.getBranch("devtools.hud.loglimit."); + prefBranch.setIntPref("console", 20); + + for (let i = 0; i < 30; i++) { + console.log("foo #" + i); // must change message to prevent repeats + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo #29", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + is(countMessageNodes(), 20, "there are 20 message nodes in the output " + + "when the log limit is set to 20"); + + console.log("bar bug585237"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bar bug585237", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + is(countMessageNodes(), 20, "there are still 20 message nodes in the " + + "output when adding one more"); + + prefBranch.setIntPref("console", 30); + for (let i = 0; i < 20; i++) { + console.log("boo #" + i); // must change message to prevent repeats + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "boo #19", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + is(countMessageNodes(), 30, "there are 30 message nodes in the output " + + "when the log limit is set to 30"); + + prefBranch.clearUserPref("console"); + + outputNode = null; +}); + +function countMessageNodes() { + return outputNode.querySelectorAll(".message").length; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js new file mode 100644 index 000000000..8945d2fa6 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js @@ -0,0 +1,48 @@ +/* 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/. */ + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-585956-console-trace.html"; + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + let hud = yield openConsole(tab); + + content.location = TEST_URI; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.trace output", + consoleTrace: { + file: "test-bug-585956-console-trace.html", + fn: "window.foobar585956c", + }, + }], + }); + + let node = [...result.matched][0]; + ok(node, "found trace log node"); + + let obj = node._messageObject; + ok(obj, "console.trace message object"); + + // The expected stack trace object. + let stacktrace = [ + { columnNumber: 2, filename: TEST_URI, functionName: "window.foobar585956c", language: 2, lineNumber: 9 }, + { columnNumber: 9, filename: TEST_URI, functionName: "foobar585956b", language: 2, lineNumber: 14 }, + { columnNumber: 9, filename: TEST_URI, functionName: "foobar585956a", language: 2, lineNumber: 18 }, + { columnNumber: 0, filename: TEST_URI, functionName: "", language: 2, lineNumber: 21 } + ]; + + ok(obj._stacktrace, "found stacktrace object"); + is(obj._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct"); + isnot(node.textContent.indexOf("bug-585956"), -1, "found file name"); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js new file mode 100644 index 000000000..7704bd627 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js @@ -0,0 +1,376 @@ +/* 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/. */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete popup keyboard usage test"; +let HUD, popup, jsterm, inputNode, completeNode; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield consoleOpened(hud); + yield popupHideAfterTab(); + yield testReturnKey(); + yield dontShowArrayNumbers(); + yield testReturnWithNoSelection(); + yield popupHideAfterReturnWithNoSelection(); + yield testCompletionInText(); + yield popupHideAfterCompletionInText(); + + HUD = popup = jsterm = inputNode = completeNode = null; +}); + +let consoleOpened = Task.async(function*(aHud) { + let deferred = promise.defer(); + HUD = aHud; + info("web console opened"); + + jsterm = HUD.jsterm; + + yield jsterm.execute("window.foobarBug585991={" + + "'item0': 'value0'," + + "'item1': 'value1'," + + "'item2': 'value2'," + + "'item3': 'value3'" + + "}"); + yield jsterm.execute("window.testBug873250a = 'hello world';" + + "window.testBug873250b = 'hello world 2';"); + popup = jsterm.autocompletePopup; + completeNode = jsterm.completeNode; + inputNode = jsterm.inputNode; + + ok(!popup.isOpen, "popup is not open"); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); + + ok(popup.isOpen, "popup is open"); + + // 4 values, and the following properties: + // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ + // hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString + // toSource unwatch valueOf watch constructor. + is(popup.itemCount, 18, "popup.itemCount is correct"); + + let sameItems = popup.getItems().reverse().map(function(e) {return e.label;}); + ok(sameItems.every(function(prop, index) { + return [ + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "item0", + "item1", + "item2", + "item3", + "propertyIsEnumerable", + "toLocaleString", + "toSource", + "toString", + "unwatch", + "valueOf", + "watch", + ][index] === prop}), "getItems returns the items we expect"); + + is(popup.selectedIndex, 17, + "Index of the first item from bottom is selected."); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem.label, "valueOf", "valueOf is selected"); + is(completeNode.value, prefix + "valueOf", + "completeNode.value holds valueOf"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); + + let currentSelectionIndex = popup.selectedIndex; + + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + + ok(popup.selectedIndex > currentSelectionIndex, + "Index is greater after PGDN"); + + currentSelectionIndex = popup.selectedIndex; + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + + ok(popup.selectedIndex < currentSelectionIndex, "Index is less after Page UP"); + + EventUtils.synthesizeKey("VK_END", {}); + is(popup.selectedIndex, 17, "index is last after End"); + + EventUtils.synthesizeKey("VK_HOME", {}); + is(popup.selectedIndex, 0, "index is first after Home"); + + info("press Tab and wait for popup to hide"); + popup._panel.addEventListener("popuphidden", function popupHidden() { + popup._panel.removeEventListener("popuphidden", popupHidden, false); + deferred.resolve(); + }, false); + EventUtils.synthesizeKey("VK_TAB", {}); + }, false); + + info("wait for completion: window.foobarBug585991."); + jsterm.setInputValue("window.foobarBug585991"); + EventUtils.synthesizeKey(".", {}); + + return deferred.promise; +}); + +function popupHideAfterTab() +{ + let deferred = promise.defer(); + + // At this point the completion suggestion should be accepted. + ok(!popup.isOpen, "popup is not open"); + + is(inputNode.value, "window.foobarBug585991.watch", + "completion was successful after VK_TAB"); + + ok(!completeNode.value, "completeNode is empty"); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 18, "popup.itemCount is correct"); + + is(popup.selectedIndex, 17, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); + + popup._panel.addEventListener("popuphidden", function onHidden() { + popup._panel.removeEventListener("popuphidden", onHidden, false); + + ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); + + is(inputNode.value, "window.foobarBug585991.", + "completion was cancelled"); + + ok(!completeNode.value, "completeNode is empty"); + + deferred.resolve(); + }, false); + + info("press Escape to close the popup"); + executeSoon(function() { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + }, false); + + info("wait for completion: window.foobarBug585991."); + executeSoon(function() { + jsterm.setInputValue("window.foobarBug585991"); + EventUtils.synthesizeKey(".", {}); + }); + + return deferred.promise; +} + +function testReturnKey() +{ + let deferred = promise.defer(); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 18, "popup.itemCount is correct"); + + is(popup.selectedIndex, 17, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem.label, "valueOf", "valueOf is selected"); + is(completeNode.value, prefix + "valueOf", + "completeNode.value holds valueOf"); + + popup._panel.addEventListener("popuphidden", function onHidden() { + popup._panel.removeEventListener("popuphidden", onHidden, false); + + ok(!popup.isOpen, "popup is not open after VK_RETURN"); + + is(inputNode.value, "window.foobarBug585991.valueOf", + "completion was successful after VK_RETURN"); + + ok(!completeNode.value, "completeNode is empty"); + + deferred.resolve(); + }, false); + + info("press Return to accept suggestion. wait for popup to hide"); + + executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {})); + }, false); + + info("wait for completion suggestions: window.foobarBug585991."); + + executeSoon(function() { + jsterm.setInputValue("window.foobarBug58599"); + EventUtils.synthesizeKey("1", {}); + EventUtils.synthesizeKey(".", {}); + }); + + return deferred.promise; +} + +function dontShowArrayNumbers() +{ + let deferred = promise.defer(); + + info("dontShowArrayNumbers"); + content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"]; + + let jsterm = HUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); + + let sameItems = popup.getItems().map(function(e) {return e.label;}); + ok(!sameItems.some(function(prop, index) { prop === "0"; }), + "Completing on an array doesn't show numbers."); + + popup._panel.addEventListener("popuphidden", function popupHidden() { + popup._panel.removeEventListener("popuphidden", popupHidden, false); + deferred.resolve(); + }, false); + + info("wait for popup to hide"); + executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {})); + }, false); + + info("wait for popup to show"); + executeSoon(() => { + jsterm.setInputValue("window.foobarBug585991"); + EventUtils.synthesizeKey(".", {}); + }); + + return deferred.promise; +} + +function testReturnWithNoSelection() +{ + let deferred = promise.defer(); + + info("test pressing return with open popup, but no selection, see bug 873250"); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown); + + ok(popup.isOpen, "popup is open"); + is(popup.itemCount, 2, "popup.itemCount is correct"); + isnot(popup.selectedIndex, -1, "popup.selectedIndex is correct"); + + info("press Return and wait for popup to hide"); + popup._panel.addEventListener("popuphidden", function popupHidden() { + popup._panel.removeEventListener("popuphidden", popupHidden); + deferred.resolve(); + }); + executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {})); + }); + + executeSoon(() => { + info("wait for popup to show"); + jsterm.setInputValue("window.testBu"); + EventUtils.synthesizeKey("g", {}); + }); + + return deferred.promise; +} + +function popupHideAfterReturnWithNoSelection() +{ + ok(!popup.isOpen, "popup is not open after VK_RETURN"); + + is(inputNode.value, "", "inputNode is empty after VK_RETURN"); + is(completeNode.value, "", "completeNode is empty"); + is(jsterm.history[jsterm.history.length-1], "window.testBug", + "jsterm history is correct"); + + return promise.resolve(); +} + +function testCompletionInText() +{ + info("test that completion works inside text, see bug 812618"); + + let deferred = promise.defer(); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown); + + ok(popup.isOpen, "popup is open"); + is(popup.itemCount, 2, "popup.itemCount is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + is(popup.selectedIndex, 0, "popup.selectedIndex is correct"); + ok(!completeNode.value, "completeNode.value is empty"); + + let items = popup.getItems().reverse().map(e => e.label); + let sameItems = items.every((prop, index) => + ["testBug873250a", "testBug873250b"][index] === prop); + ok(sameItems, "getItems returns the items we expect"); + + info("press Tab and wait for popup to hide"); + popup._panel.addEventListener("popuphidden", function popupHidden() { + popup._panel.removeEventListener("popuphidden", popupHidden); + deferred.resolve(); + }); + EventUtils.synthesizeKey("VK_TAB", {}); + }); + + jsterm.setInputValue("dump(window.testBu)"); + inputNode.selectionStart = inputNode.selectionEnd = 18; + EventUtils.synthesizeKey("g", {}); + return deferred.promise; +} + +function popupHideAfterCompletionInText() +{ + // At this point the completion suggestion should be accepted. + ok(!popup.isOpen, "popup is not open"); + is(inputNode.value, "dump(window.testBug873250b)", + "completion was successful after VK_TAB"); + is(inputNode.selectionStart, 26, "cursor location is correct"); + is(inputNode.selectionStart, inputNode.selectionEnd, "cursor location (confirmed)"); + ok(!completeNode.value, "completeNode is empty"); + + return promise.resolve(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js new file mode 100644 index 000000000..76fc5d324 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js @@ -0,0 +1,119 @@ +/* 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/. */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete popup test"; + +"use strict"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield consoleOpened(hud); +}); + +function consoleOpened(HUD) { + let deferred = promise.defer(); + + let items = [ + {label: "item0", value: "value0"}, + {label: "item1", value: "value1"}, + {label: "item2", value: "value2"}, + ]; + + let popup = HUD.jsterm.autocompletePopup; + + let input = popup._document.activeElement; + function getActiveDescendant() { + return input.ownerDocument.getElementById( + input.getAttribute("aria-activedescendant")); + } + + ok(!popup.isOpen, "popup is not open"); + ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant"); + + popup._panel.addEventListener("popupshown", function() { + popup._panel.removeEventListener("popupshown", arguments.callee, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 0, "no items"); + ok(!input.hasAttribute("aria-activedescendant"), + "no aria-activedescendant"); + + popup.setItems(items); + + is(popup.itemCount, items.length, "items added"); + + let sameItems = popup.getItems(); + is(sameItems.every(function(aItem, aIndex) { + return aItem === items[aIndex]; + }), true, "getItems returns back the same items"); + + is(popup.selectedIndex, 2, + "Index of the first item from bottom is selected."); + is(popup.selectedItem, items[2], "First item from bottom is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + popup.selectedIndex = 1; + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + popup.selectedItem = items[2]; + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works"); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + is(popup.selectNextItem(), items[2], "selectPreviousItem() works"); + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + ok(popup.selectNextItem(), "selectPreviousItem() works"); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem, items[0], "item0 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + items.push({label: "label3", value: "value3"}); + popup.appendItem(items[3]); + + is(popup.itemCount, items.length, "item3 appended"); + + popup.selectedIndex = 3; + is(popup.selectedItem, items[3], "item3 is selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + + popup.removeItem(items[2]); + + is(popup.selectedIndex, 2, "index2 is selected"); + is(popup.selectedItem, items[3], "item3 is still selected"); + ok(getActiveDescendant().selected, "aria-activedescendant is correct"); + is(popup.itemCount, items.length - 1, "item2 removed"); + + popup.clearItems(); + is(popup.itemCount, 0, "items cleared"); + ok(!input.hasAttribute("aria-activedescendant"), + "no aria-activedescendant"); + + popup.hidePopup(); + deferred.resolve(); + }, false); + + popup.openPopup(); + + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js b/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js new file mode 100644 index 000000000..5512201cf --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_586388_select_all.js @@ -0,0 +1,91 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/"; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testSelectionWhenMovingBetweenBoxes(hud); + performTestsAfterOutput(hud); +}) + +let testSelectionWhenMovingBetweenBoxes = Task.async(function *(aHud) { + let hud = aHud; + let jsterm = hud.jsterm; + + // Fill the console with some output. + jsterm.clearOutput(); + yield jsterm.execute("1 + 2"); + yield jsterm.execute("3 + 4"); + yield jsterm.execute("5 + 6"); + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "3", + category: CATEGORY_OUTPUT, + }, + { + text: "7", + category: CATEGORY_OUTPUT, + }, + { + text: "11", + category: CATEGORY_OUTPUT, + }], + }); +}); + +function performTestsAfterOutput(aHud) { + let hud = aHud; + let outputNode = hud.outputNode; + + ok(outputNode.childNodes.length >= 3, "the output node has children after " + + "executing some JavaScript"); + + // Test that the global Firefox "Select All" functionality (e.g. Edit > + // Select All) works properly in the Web Console. + let commandController = hud.ui._commandController; + ok(commandController != null, "the window has a command controller object"); + + commandController.selectAll(); + + let selectedCount = hud.ui.output.getSelectedMessages().length; + is(selectedCount, outputNode.childNodes.length, + "all console messages are selected after performing a regular browser " + + "select-all operation"); + + hud.iframeWindow.getSelection().removeAllRanges(); + + // Test the context menu "Select All" (which has a different code path) works + // properly as well. + let contextMenuId = outputNode.parentNode.getAttribute("context"); + let contextMenu = hud.ui.document.getElementById(contextMenuId); + ok(contextMenu != null, "the output node has a context menu"); + + let selectAllItem = contextMenu.querySelector("*[command='cmd_selectAll']"); + ok(selectAllItem != null, + "the context menu on the output node has a \"Select All\" item"); + + outputNode.focus(); + + selectAllItem.doCommand(); + + selectedCount = hud.ui.output.getSelectedMessages().length; + is(selectedCount, outputNode.childNodes.length, + "all console messages are selected after performing a select-all " + + "operation from the context menu"); + + hud.iframeWindow.getSelection().removeAllRanges(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js new file mode 100644 index 000000000..78a4221c4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js @@ -0,0 +1,98 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let HUD, outputNode; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield consoleOpened(hud); + yield testContextMenuCopy(); + + HUD = outputNode = null; +}); + +function consoleOpened(aHud) { + HUD = aHud; + + let deferred = promise.defer(); + + // See bugs 574036, 586386 and 587617. + outputNode = HUD.outputNode; + + HUD.jsterm.clearOutput(); + + let controller = top.document.commandDispatcher. + getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled"); + + content.console.log("Hello world! bug587617"); + + waitForMessages({ + webconsole: HUD, + messages: [{ + text: "bug587617", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(([result]) => { + let msg = [...result.matched][0]; + HUD.ui.output.selectMessage(msg); + + outputNode.focus(); + + goUpdateCommand("cmd_copy"); + controller = top.document.commandDispatcher.getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled"); + + // Remove new lines since getSelection() includes one between message and line + // number, but the clipboard doesn't (see bug 1119503) + let selection = (HUD.iframeWindow.getSelection() + "").replace(/\r?\n|\r/g, " "); + isnot(selection.indexOf("bug587617"), -1, + "selection text includes 'bug587617'"); + + waitForClipboard((str) => { return selection.trim() == str.trim(); }, + () => { goDoCommand("cmd_copy") }, + deferred.resolve, deferred.resolve); + }); + return deferred.promise; +} + +// Test that the context menu "Copy" (which has a different code path) works +// properly as well. +function testContextMenuCopy() { + let deferred = promise.defer(); + + let contextMenuId = outputNode.parentNode.getAttribute("context"); + let contextMenu = HUD.ui.document.getElementById(contextMenuId); + ok(contextMenu, "the output node has a context menu"); + + let copyItem = contextMenu.querySelector("*[command='cmd_copy']"); + ok(copyItem, "the context menu on the output node has a \"Copy\" item"); + + // Remove new lines since getSelection() includes one between message and line + // number, but the clipboard doesn't (see bug 1119503) + let selection = (HUD.iframeWindow.getSelection() + "").replace(/\r?\n|\r/g, " "); + + copyItem.doCommand(); + + waitForClipboard((str) => { return selection.trim() == str.trim(); }, + () => { goDoCommand("cmd_copy") }, + deferred.resolve, deferred.resolve); + HUD = outputNode = null; + + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js new file mode 100644 index 000000000..6e7fcc1f6 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588342_document_focus.js @@ -0,0 +1,39 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 588342"; + +let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + +"use strict"; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield consoleOpened(hud); + + is(fm.focusedWindow, browser.contentWindow, + "content document has focus"); + + fm = null; +}); + +function consoleOpened(hud) { + let deferred = promise.defer(); + waitForFocus(function() { + is(hud.jsterm.inputNode.getAttribute("focused"), "true", + "jsterm input is focused on web console open"); + isnot(fm.focusedWindow, content, "content document has no focus"); + closeConsole(null).then(deferred.resolve); + }, hud.iframeWindow); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js new file mode 100644 index 000000000..ef1c3e5a2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js @@ -0,0 +1,53 @@ +/* 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 adding text to one of the output labels doesn't cause errors. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield testTextNodeInsertion(hud); +}); + +// Test for bug 588730: Adding a text node to an existing label element causes +// warnings +function testTextNodeInsertion(hud) { + let deferred = promise.defer(); + let outputNode = hud.outputNode; + + let label = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "label"); + outputNode.appendChild(label); + + let error = false; + let listener = { + observe: function(aMessage) { + let messageText = aMessage.message; + if (messageText.indexOf("JavaScript Warning") !== -1) { + error = true; + } + } + }; + + Services.console.registerListener(listener); + + // This shouldn't fail. + label.appendChild(document.createTextNode("foo")); + + executeSoon(function() { + Services.console.unregisterListener(listener); + ok(!error, "no error when adding text nodes as children of labels"); + + return deferred.resolve(); + }); + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js new file mode 100644 index 000000000..9b9fb3168 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588967_input_expansion.js @@ -0,0 +1,45 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + testInputExpansion(hud); +}); + + +function testInputExpansion(hud) { + let input = hud.jsterm.inputNode; + + input.focus(); + + is(input.getAttribute("multiline"), "true", "multiline is enabled"); + + let ordinaryHeight = input.clientHeight; + + // Tests if the inputNode expands. + input.value = "hello\nworld\n"; + let length = input.value.length; + input.selectionEnd = length; + input.selectionStart = length; + // Performs an "d". This will trigger/test for the input event that should + // change the height of the inputNode. + EventUtils.synthesizeKey("d", {}); + ok(input.clientHeight > ordinaryHeight, "the input expanded"); + + // Test if the inputNode shrinks again. + input.value = ""; + EventUtils.synthesizeKey("d", {}); + is(input.clientHeight, ordinaryHeight, "the input's height is normal again"); + + input = length = null; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js b/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js new file mode 100644 index 000000000..72fd295eb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js @@ -0,0 +1,49 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +"use strict" + +const TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" + + "foobarCssParser:baz'>test CSS parser filter</div>"; + +/** + * Unit test for bug 589162: + * CSS filtering on the console does not work + */ +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + + // CSS warnings are disabled by default. + hud.setFilterState("cssparser", true); + hud.jsterm.clearOutput(); + + content.location.reload(); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarCssParser", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }], + }); + + hud.setFilterState("cssparser", false); + + let msg = "the unknown CSS property warning is not displayed, " + + "after filtering"; + testLogEntry(hud.outputNode, "foobarCssParser", msg, true, true); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js b/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js new file mode 100644 index 000000000..c29e61b26 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js @@ -0,0 +1,37 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Julian Viereck <jviereck@mozilla.com> + * Patrick Walton <pcwalton@mozilla.com> + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that, when the user types an extraneous closing bracket, no error +// appears. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,test for bug 592442"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + let jsterm = hud.jsterm; + + jsterm.setInputValue("document.getElementById)"); + + let error = false; + try { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY); + } + catch (ex) { + error = true; + } + + ok(!error, "no error was thrown when an extraneous bracket was inserted"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js b/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js new file mode 100644 index 000000000..ac526b502 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js @@ -0,0 +1,65 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html"; + +const TEST_IFRAME_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"; + +const TEST_DUMMY_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let tab1, tab2; + +function test() { + loadTab(TEST_URI).then(({tab}) => { + tab1 = tab; + + content.console.log("FOO"); + openConsole().then(() => { + tab2 = gBrowser.addTab(TEST_DUMMY_URI); + gBrowser.selectedTab = tab2; + gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true); + }); + }); +} + +function tab2Loaded(aEvent) { + tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true); + + openConsole(gBrowser.selectedTab).then(() => { + tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true); + tab1.linkedBrowser.contentWindow.location.reload(); + }); +} + +function tab1Reloaded(aEvent) { + tab1.linkedBrowser.removeEventListener(aEvent.type, tab1Reloaded, true); + + let hud1 = HUDService.getHudByWindow(tab1.linkedBrowser.contentWindow); + let outputNode1 = hud1.outputNode; + + waitForMessages({ + webconsole: hud1, + messages: [{ + text: TEST_IFRAME_URI, + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }).then(() => { + let hud2 = HUDService.getHudByWindow(tab2.linkedBrowser.contentWindow); + let outputNode2 = hud2.outputNode; + + isnot(outputNode1, outputNode2, + "the two HUD outputNodes must be different"); + + let msg = "Didn't find the iframe network request in tab2"; + testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true); + + closeConsole(tab2).then(() => { + gBrowser.removeTab(tab2); + tab1 = tab2 = null; + executeSoon(finishTest); + }); + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js new file mode 100644 index 000000000..c6a983d6b --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js @@ -0,0 +1,131 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +let HUD; +let outputItem; +let outputNode; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + HUD = yield openConsole(); + outputNode = HUD.outputNode; + + // reload the tab + BrowserReload(); + yield loadBrowser(gBrowser.selectedBrowser); + + let event = yield clickEvents(); + yield testClickAgain(event); + yield networkPanelHidden(); + + HUD = outputItem = outputNode = null; +}); + +function clickEvents() { + let deferred = promise.defer(); + + waitForMessages({ + webconsole: HUD, + messages: [{ + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }).then(([result]) => { + let msg = [...result.matched][0]; + outputItem = msg.querySelector(".message-body .url"); + ok(outputItem, "found a network message"); + document.addEventListener("popupshown", function onPanelShown(event) { + document.removeEventListener("popupshown", onPanelShown, false); + deferred.resolve(event); + }, false); + + // Send the mousedown and click events such that the network panel opens. + EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); + EventUtils.sendMouseEvent({type: "click"}, outputItem); + }); + + return deferred.promise; +} + +function testClickAgain(event) { + info("testClickAgain"); + + let deferred = promise.defer(); + + document.addEventListener("popupshown", networkPanelShowFailure, false); + + // The network panel should not open for the second time. + EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); + EventUtils.sendMouseEvent({type: "click"}, outputItem); + + executeSoon(function() { + document.addEventListener("popuphidden", function onHidden() { + document.removeEventListener("popuphidden", onHidden, false); + deferred.resolve(); + }, false); + event.target.hidePopup(); + }); + + return deferred.promise; +} + +function networkPanelShowFailure() { + ok(false, "the network panel should not show"); +} + +function networkPanelHidden() { + let deferred = promise.defer(); + + info("networkPanelHidden"); + + // The network panel should not show because this is a mouse event that starts + // in a position and ends in another. + EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4}, + outputItem); + EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6}, + outputItem); + + // The network panel should not show because this is a middle-click. + EventUtils.sendMouseEvent({type: "mousedown", button: 1}, + outputItem); + EventUtils.sendMouseEvent({type: "click", button: 1}, + outputItem); + + // The network panel should not show because this is a right-click. + EventUtils.sendMouseEvent({type: "mousedown", button: 2}, + outputItem); + EventUtils.sendMouseEvent({type: "click", button: 2}, + outputItem); + + executeSoon(function() { + document.removeEventListener("popupshown", networkPanelShowFailure, false); + + // Done with the network output. Now test the jsterm output and the property + // panel. + HUD.jsterm.execute("document").then((msg) => { + info("jsterm execute 'document' callback"); + + HUD.jsterm.once("variablesview-open", deferred.resolve); + let outputItem = msg.querySelector(".message-body a"); + ok(outputItem, "jsterm output message found"); + + // Send the mousedown and click events such that the property panel opens. + EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); + EventUtils.sendMouseEvent({type: "click"}, outputItem); + }); + }); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js b/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js new file mode 100644 index 000000000..38210ebfb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js @@ -0,0 +1,156 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +let inputNode, values; + +let TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 594497 and bug 619598"; +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + setup(hud); + performTests(); + + inputNode = values = null; +}); + +function setup(HUD) { + inputNode = HUD.jsterm.inputNode; + + inputNode.focus(); + + ok(!inputNode.value, "inputNode.value is empty"); + + values = ["document", "window", "document.body"]; + values.push(values.join(";\n"), "document.location"); + + // Execute each of the values; + for (let i = 0; i < values.length; i++) { + HUD.jsterm.setInputValue(values[i]); + HUD.jsterm.execute(); + } +} + +function performTests() { + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[4], + "VK_UP: inputNode.value #4 is correct"); + + ok(inputNode.selectionStart == values[4].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[3], + "VK_UP: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart == values[3].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + inputNode.setSelectionRange(values[3].length - 2, values[3].length - 2); + + EventUtils.synthesizeKey("VK_UP", {}); + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[3], + "VK_UP two times: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart == inputNode.value.indexOf("\n") && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[3], + "VK_UP again: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart == 0 && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[2], + "VK_UP: inputNode.value #2 is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[1], + "VK_UP: inputNode.value #1 is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(inputNode.value, values[0], + "VK_UP: inputNode.value #0 is correct"); + + ok(inputNode.selectionStart == values[0].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[1], + "VK_DOWN: inputNode.value #1 is correct"); + + ok(inputNode.selectionStart == values[1].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[2], + "VK_DOWN: inputNode.value #2 is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[3], + "VK_DOWN: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart == values[3].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + inputNode.setSelectionRange(2, 2); + + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[3], + "VK_DOWN two times: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart > inputNode.value.lastIndexOf("\n") && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[3], + "VK_DOWN again: inputNode.value #3 is correct"); + + ok(inputNode.selectionStart == values[3].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(inputNode.value, values[4], + "VK_DOWN: inputNode.value #4 is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + ok(!inputNode.value, + "VK_DOWN: inputNode.value is empty"); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js new file mode 100644 index 000000000..fe8ff7b2c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js @@ -0,0 +1,64 @@ +/* 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/. */ + +"use strict"; + +const PREF = "devtools.webconsole.persistlog"; +const TEST_FILE = "test-network.html"; +const TEST_URI = "data:text/html;charset=utf8,<p>test file URI"; + +let hud; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref(PREF, true); + + let jar = getJar(getRootDirectory(gTestPath)); + let dir = jar ? + extractJarToTmp(jar) : + getChromeDir(getResolvedURI(gTestPath)); + + dir.append(TEST_FILE); + let uri = Services.io.newFileURI(dir); + + let { browser } = yield loadTab(TEST_URI); + + hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + content.location = uri.spec; + yield loaded; + + yield testMessages(); + + Services.prefs.clearUserPref(PREF); + hud = null; +}); + +function testMessages() { + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "running network console logging tests", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "test-network.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "test-image.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "testscript.js", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }) +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js new file mode 100644 index 000000000..cefb3e377 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js @@ -0,0 +1,101 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that the Web Console doesn't leak when multiple tabs and windows are +// opened and then closed. + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350"; + +let win1 = window, win2; +let openTabs = []; +let loadedTabCount = 0; + +function test() { + requestLongerTimeout(3); + + // Add two tabs in the main window. + addTabs(win1); + + // Open a new window. + win2 = OpenBrowserWindow(); + win2.addEventListener("load", onWindowLoad, true); +} + +function onWindowLoad(aEvent) { + win2.removeEventListener(aEvent.type, onWindowLoad, true); + + // Add two tabs in the new window. + addTabs(win2); +} + +function addTabs(aWindow) { + for (let i = 0; i < 2; i++) { + let tab = aWindow.gBrowser.addTab(TEST_URI); + openTabs.push(tab); + + tab.linkedBrowser.addEventListener("load", function onLoad(aEvent) { + tab.linkedBrowser.removeEventListener(aEvent.type, onLoad, true); + + loadedTabCount++; + info("tabs loaded: " + loadedTabCount); + if (loadedTabCount >= 4) { + executeSoon(openConsoles); + } + }, true); + } +} + +function openConsoles() { + // open the Web Console for each of the four tabs and log a message. + let consolesOpen = 0; + for (let i = 0; i < openTabs.length; i++) { + let tab = openTabs[i]; + openConsole(tab).then(function(index, hud) { + ok(hud, "HUD is open for tab " + index); + let window = hud.target.tab.linkedBrowser.contentWindow; + window.console.log("message for tab " + index); + consolesOpen++; + if (consolesOpen == 4) { + // Use executeSoon() to allow the promise to resolve. + executeSoon(closeConsoles); + } + }.bind(null, i)); + } +} + +function closeConsoles() { + let consolesClosed = 0; + + function onWebConsoleClose(aSubject, aTopic) { + if (aTopic == "web-console-destroyed") { + consolesClosed++; + info("consoles destroyed: " + consolesClosed); + if (consolesClosed == 4) { + // Use executeSoon() to allow all the observers to execute. + executeSoon(finishTest); + } + } + } + + Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false); + + registerCleanupFunction(() => { + Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed"); + }); + + win2.close(); + + win1.gBrowser.removeTab(openTabs[0]); + win1.gBrowser.removeTab(openTabs[1]); + + openTabs = win1 = win2 = null; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js new file mode 100644 index 000000000..bfafa4423 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js @@ -0,0 +1,203 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595934 - message categories coverage."; +const TESTS_PATH = "http://example.com/browser/browser/devtools/webconsole/test/"; +const TESTS = [ + { // #0 + file: "test-bug-595934-css-loader.html", + category: "CSS Loader", + matchString: "text/css", + }, + { // #1 + file: "test-bug-595934-imagemap.html", + category: "Layout: ImageMap", + matchString: "shape=\"rect\"", + }, + { // #2 + file: "test-bug-595934-html.html", + category: "HTML", + matchString: "multipart/form-data", + onload: function() { + let form = content.document.querySelector("form"); + form.submit(); + }, + }, + { // #3 + file: "test-bug-595934-workers.html", + category: "Web Worker", + matchString: "fooBarWorker", + expectError: true, + }, + { // #4 + file: "test-bug-595934-malformedxml.xhtml", + category: "malformed-xml", + matchString: "no element found", + }, + { // #5 + file: "test-bug-595934-svg.xhtml", + category: "SVG", + matchString: "fooBarSVG", + }, + { // #6 + file: "test-bug-595934-css-parser.html", + category: "CSS Parser", + matchString: "foobarCssParser", + }, + { // #7 + file: "test-bug-595934-malformedxml-external.html", + category: "malformed-xml", + matchString: "</html>", + }, + { // #8 + file: "test-bug-595934-empty-getelementbyid.html", + category: "DOM", + matchString: "getElementById", + }, + { // #9 + file: "test-bug-595934-canvas-css.html", + category: "CSS Parser", + matchString: "foobarCanvasCssParser", + }, + { // #10 + file: "test-bug-595934-image.html", + category: "Image", + matchString: "corrupt", + }, +]; + +let pos = -1; + +let foundCategory = false; +let foundText = false; +let pageLoaded = false; +let pageError = false; +let output = null; +let jsterm = null; +let hud = null; +let testEnded = false; + +let TestObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function test_observe(aSubject) + { + if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) { + return; + } + + var expectedCategory = TESTS[pos].category; + + info("test #" + pos + " console observer got " + aSubject.category + + ", is expecting " + expectedCategory); + + if (aSubject.category == expectedCategory) { + foundCategory = true; + startNextTest(); + } + else { + info("unexpected message was: " + aSubject.sourceName + ":" + + aSubject.lineNumber + "; " + aSubject.errorMessage); + } + } +}; + +function consoleOpened(aHud) { + hud = aHud; + output = hud.outputNode; + jsterm = hud.jsterm; + + Services.console.registerListener(TestObserver); + + registerCleanupFunction(testEnd); + + testNext(); +} + +function testNext() { + jsterm.clearOutput(); + foundCategory = false; + foundText = false; + pageLoaded = false; + pageError = false; + + pos++; + info("testNext: #" + pos); + if (pos < TESTS.length) { + let test = TESTS[pos]; + + waitForMessages({ + webconsole: hud, + messages: [{ + name: "message for test #" + pos + ": '" + test.matchString +"'", + text: test.matchString, + }], + }).then(() => { + foundText = true; + startNextTest(); + }); + + let testLocation = TESTS_PATH + test.file; + gBrowser.selectedBrowser.addEventListener("load", function onLoad(aEvent) { + if (content.location.href != testLocation) { + return; + } + gBrowser.selectedBrowser.removeEventListener(aEvent.type, onLoad, true); + + pageLoaded = true; + test.onload && test.onload(aEvent); + + if (test.expectError) { + content.addEventListener("error", function _onError() { + content.removeEventListener("error", _onError); + pageError = true; + startNextTest(); + }); + expectUncaughtException(); + } + else { + pageError = true; + } + + startNextTest(); + }, true); + + content.location = testLocation; + } + else { + testEnded = true; + finishTest(); + } +} + +function testEnd() { + if (!testEnded) { + info("foundCategory " + foundCategory + " foundText " + foundText + + " pageLoaded " + pageLoaded + " pageError " + pageError); + } + + Services.console.unregisterListener(TestObserver); + hud = TestObserver = output = jsterm = null; +} + +function startNextTest() { + if (!testEnded && foundCategory && foundText && pageLoaded && pageError) { + testNext(); + } +} + +function test() { + requestLongerTimeout(2); + + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js new file mode 100644 index 000000000..6bc20ba1c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js @@ -0,0 +1,104 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let tab1, tab2, win1, win2; +let noErrors = true; + +function tab1Loaded() { + win2 = OpenBrowserWindow(); + whenDelayedStartupFinished(win2, win2Loaded); +} + +function win2Loaded() { + tab2 = win2.gBrowser.addTab(TEST_URI); + win2.gBrowser.selectedTab = tab2; + tab2.linkedBrowser.addEventListener("load", tab2Loaded, true); +} + +function tab2Loaded(aEvent) { + tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true); + + let consolesOpened = 0; + function onWebConsoleOpen() { + consolesOpened++; + if (consolesOpened == 2) { + executeSoon(closeConsoles); + } + } + + function openConsoles() { + try { + let target1 = TargetFactory.forTab(tab1); + gDevTools.showToolbox(target1, "webconsole").then(onWebConsoleOpen); + } + catch (ex) { + ok(false, "gDevTools.showToolbox(target1) exception: " + ex); + noErrors = false; + } + + try { + let target2 = TargetFactory.forTab(tab2); + gDevTools.showToolbox(target2, "webconsole").then(onWebConsoleOpen); + } + catch (ex) { + ok(false, "gDevTools.showToolbox(target2) exception: " + ex); + noErrors = false; + } + } + + function closeConsoles() { + try { + let target1 = TargetFactory.forTab(tab1); + gDevTools.closeToolbox(target1).then(function() { + try { + let target2 = TargetFactory.forTab(tab2); + gDevTools.closeToolbox(target2).then(testEnd); + } + catch (ex) { + ok(false, "gDevTools.closeToolbox(target2) exception: " + ex); + noErrors = false; + } + }); + } + catch (ex) { + ok(false, "gDevTools.closeToolbox(target1) exception: " + ex); + noErrors = false; + } + } + + function testEnd() { + ok(noErrors, "there were no errors"); + + win1.gBrowser.removeTab(tab1); + + Array.forEach(win2.gBrowser.tabs, function(aTab) { + win2.gBrowser.removeTab(aTab); + }); + + executeSoon(function() { + win2.close(); + tab1 = tab2 = win1 = win2 = null; + finishTest(); + }); + } + + waitForFocus(openConsoles, tab2.linkedBrowser.contentWindow); +} + +function test() { + loadTab(TEST_URI).then(() => { + tab1 = gBrowser.selectedTab; + win1 = window; + tab1Loaded(); + }); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js new file mode 100644 index 000000000..fa8905af0 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js @@ -0,0 +1,36 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/" + + "webconsole/test/test-bug-597136-external-script-" + + "errors.html"; + +function test() { + Task.spawn(function* () { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + + let button = content.document.querySelector("button"); + + expectUncaughtException(); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bogus is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js new file mode 100644 index 000000000..bf745ac3d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that network requests from chrome don't cause the Web Console to +// throw exceptions. + +const TEST_URI = "http://example.com/"; + +let good = true; +let listener = { + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]), + observe: function(aSubject, aTopic, aData) { + if (aSubject instanceof Ci.nsIScriptError && + aSubject.category === "XPConnect JavaScript" && + aSubject.sourceName.contains("webconsole")) { + good = false; + } + } +}; + +let xhr; + +function test() { + Services.console.registerListener(listener); + + HUDService; // trigger a lazy-load of the HUD Service + + xhr = new XMLHttpRequest(); + xhr.addEventListener("load", xhrComplete, false); + xhr.open("GET", TEST_URI, true); + xhr.send(null); +} + +function xhrComplete() { + xhr.removeEventListener("load", xhrComplete, false); + window.setTimeout(checkForException, 0); +} + +function checkForException() { + ok(good, "no exception was thrown when sending a network request from a " + + "chrome window"); + + Services.console.unregisterListener(listener); + listener = xhr = 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 new file mode 100644 index 000000000..f8fc821eb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js @@ -0,0 +1,82 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network.html"; +const PREF = "devtools.webconsole.persistlog"; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref(PREF, true); + + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let results = yield consoleOpened(hud); + + testScroll(results, hud); + + Services.prefs.clearUserPref(PREF); +}); + +function consoleOpened(hud) { + let deferred = promise.defer(); + + for (let i = 0; i < 200; i++) { + content.console.log("test message " + i); + } + + hud.setFilterState("network", false); + hud.setFilterState("networkinfo", false); + + hud.ui.filterBox.value = "test message"; + hud.ui.adjustVisibilityOnSearchStringChange(); + + waitForMessages({ + webconsole: hud, + messages: [{ + name: "console messages displayed", + text: "test message 199", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-network.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }).then(deferred.resolve); + + content.location.reload(); + }); + + return deferred.promise; +} + +function testScroll([result], hud) { + let scrollNode = hud.outputNode.parentNode; + let msgNode = [...result.matched][0]; + ok(msgNode.classList.contains("filtered-by-type"), + "network message is filtered by type"); + ok(msgNode.classList.contains("filtered-by-string"), + "network message is filtered by string"); + + ok(scrollNode.scrollTop > 0, "scroll location is not at the top"); + + // Make sure the Web Console output is scrolled as near as possible to the + // bottom. + let nodeHeight = msgNode.clientHeight; + ok(scrollNode.scrollTop >= scrollNode.scrollHeight - scrollNode.clientHeight - + nodeHeight * 2, "scroll location is correct"); + + hud.setFilterState("network", true); + hud.setFilterState("networkinfo", true); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js new file mode 100644 index 000000000..ccaedc98e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js @@ -0,0 +1,62 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html"; + +let HUD; + +let test = asyncTest(function* () { + expectUncaughtException(); + + let { browser } = yield loadTab(TEST_URI); + HUD = yield openConsole(); + + expectUncaughtException(); + + yield reload(browser); + + yield testMessages(); + + yield closeConsole(); + + // Close and reopen + gBrowser.removeCurrentTab(); + + expectUncaughtException(); + + let tab = yield loadTab(TEST_URI); + HUD = yield openConsole(); + + expectUncaughtException(); + + yield reload(tab.browser); + + yield testMessages(); + + HUD = null; +}); + +function reload(browser) { + let loaded = loadBrowser(browser); + content.location.reload(); + return loaded; +} + +function testMessages() { + return waitForMessages({ + webconsole: HUD, + messages: [{ + name: "error message displayed", + text: "fooBug597756_error", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js new file mode 100644 index 000000000..eb3c26cf0 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js @@ -0,0 +1,87 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const INIT_URI = "data:text/plain;charset=utf8,hello world"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs"; + +let loads = 0; +function performTest(aRequest, aConsole) +{ + let deferred = promise.defer(); + + let headers = null; + + function readHeader(aName) + { + for (let header of headers) { + if (header.name == aName) { + return header.value; + } + } + return null; + } + + aConsole.webConsoleClient.getResponseHeaders(aRequest.actor, + function (aResponse) { + headers = aResponse.headers; + ok(headers, "we have the response headers for reload"); + + let contentType = readHeader("Content-Type"); + let contentLength = readHeader("Content-Length"); + + ok(!contentType, "we do not have the Content-Type header"); + isnot(contentLength, 60, "Content-Length != 60"); + + if (contentType || contentLength == 60) { + console.debug("lastFinishedRequest", lastFinishedRequest, + "request", lastFinishedRequest.request, + "response", lastFinishedRequest.response, + "updates", lastFinishedRequest.updates, + "response headers", headers); + } + + executeSoon(deferred.resolve); + }); + + HUDService.lastFinishedRequest.callback = null; + + return deferred.promise; +} + +function waitForRequest() { + let deferred = promise.defer(); + HUDService.lastFinishedRequest.callback = (req, console) => { + loads++; + ok(req, "page load was logged"); + if (loads != 2) { + return; + } + performTest(req, console).then(deferred.resolve); + }; + return deferred.promise; +} + +let test = asyncTest(function* () { + let { browser } = yield loadTab(INIT_URI); + + let hud = yield openConsole(); + + let gotLastRequest = waitForRequest(); + + let loaded = loadBrowser(browser); + content.location = TEST_URI; + yield loaded; + + let reloaded = loadBrowser(browser); + content.location.reload(); + yield reloaded; + + yield gotLastRequest; +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js new file mode 100644 index 000000000..bd224375f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js @@ -0,0 +1,66 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const INIT_URI = "data:text/html;charset=utf-8,Web Console - bug 600183 test"; +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html"; + +function performTest(lastFinishedRequest, aConsole) +{ + let deferred = promise.defer(); + + ok(lastFinishedRequest, "charset test page was loaded and logged"); + HUDService.lastFinishedRequest.callback = null; + + executeSoon(() => { + aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor, + (aResponse) => { + ok(!aResponse.contentDiscarded, "response body was not discarded"); + + let body = aResponse.content.text; + ok(body, "we have the response body"); + + let chars = "\u7684\u95ee\u5019!"; // 的问候! + isnot(body.indexOf("<p>" + chars + "</p>"), -1, + "found the chinese simplified string"); + + HUDService.lastFinishedRequest.callback = null; + executeSoon(deferred.resolve); + }); + }); + + return deferred.promise; +} + +function waitForRequest() { + let deferred = promise.defer(); + HUDService.lastFinishedRequest.callback = (req, console) => { + performTest(req, console).then(deferred.resolve); + }; + return deferred.promise; +} + +let test = asyncTest(function* () { + let { browser } = yield loadTab(INIT_URI); + + let hud = yield openConsole(); + + yield hud.ui.setSaveRequestAndResponseBodies(true); + + ok(hud.ui._saveRequestAndResponseBodies, + "The saveRequestAndResponseBodies property was successfully set."); + + let gotLastRequest = waitForRequest(); + + let loaded = loadBrowser(browser); + content.location = TEST_URI; + yield loaded; + + yield gotLastRequest; +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js new file mode 100644 index 000000000..dbe21f32d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js @@ -0,0 +1,73 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 601177: log levels"; +const TEST_URI2 = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-601177-log-levels.html"; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref("javascript.options.strict", true); + + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + expectUncaughtException(); + + yield testLogLevels(hud); + + Services.prefs.clearUserPref("javascript.options.strict"); +}); + +function testLogLevels(hud) { + content.location = TEST_URI2; + + info("waiting for messages"); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + text: "test-bug-601177-log-levels.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "test-bug-601177-log-levels.js", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "test-image.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "foobar-known-to-fail.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_ERROR, + }, + { + text: "foobarBug601177exception", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "undefinedPropertyBug601177", + category: CATEGORY_JS, + severity: SEVERITY_WARNING, + }, + { + text: "foobarBug601177strictError", + category: CATEGORY_JS, + severity: SEVERITY_WARNING, + }, + ], + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js new file mode 100644 index 000000000..2ff267896 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js @@ -0,0 +1,69 @@ +/* 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/. */ + +// Test that the console output scrolls to JS eval results when there are many +// messages displayed. See bug 601352. + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test for bug 601352"); + let hud = yield openConsole(tab); + hud.jsterm.clearOutput(); + + let longMessage = ""; + for (let i = 0; i < 50; i++) { + longMessage += "LongNonwrappingMessage"; + } + + for (let i = 0; i < 50; i++) { + content.console.log("test1 message " + i); + } + + content.console.log(longMessage); + + for (let i = 0; i < 50; i++) { + content.console.log("test2 message " + i); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test1 message 0", + }, { + text: "test1 message 49", + }, { + text: "LongNonwrappingMessage", + }, { + text: "test2 message 0", + }, { + text: "test2 message 49", + }], + }); + + let node = yield hud.jsterm.execute("1+1"); + + let scrollNode = hud.outputNode.parentNode; + let rectNode = node.getBoundingClientRect(); + let rectOutput = scrollNode.getBoundingClientRect(); + console.debug("rectNode", rectNode, "rectOutput", rectOutput); + console.log("scrollNode scrollHeight", scrollNode.scrollHeight, "scrollTop", scrollNode.scrollTop, "clientHeight", scrollNode.clientHeight); + + isnot(scrollNode.scrollTop, 0, "scroll location is not at the top"); + + // The bounding client rect .top/left coordinates are relative to the + // console iframe. + + // Visible scroll viewport. + let height = rectOutput.height; + + // Top and bottom coordinates of the last message node, relative to the outputNode. + let top = rectNode.top - rectOutput.top; + let bottom = top + rectNode.height; + info("node top " + top + " node bottom " + bottom + " node clientHeight " + node.clientHeight); + + ok(top >= 0 && bottom <= height, "last message is visible"); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js new file mode 100644 index 000000000..d00f2d2d9 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js @@ -0,0 +1,241 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the filter button UI logic works correctly. + +const TEST_URI = "http://example.com/"; + +let hud, hudId, hudBox; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + hudId = hud.hudId; + hudBox = hud.ui.rootElement; + + testFilterButtons(); + + hud = hudId = hudBox = null; +}); + +function testFilterButtons() { + testMenuFilterButton("net"); + testMenuFilterButton("css"); + testMenuFilterButton("js"); + testMenuFilterButton("logging"); + testMenuFilterButton("security"); + + testIsolateFilterButton("net"); + testIsolateFilterButton("css"); + testIsolateFilterButton("js"); + testIsolateFilterButton("logging"); + testIsolateFilterButton("security"); +} + +function testMenuFilterButton(aCategory) { + let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]"; + let button = hudBox.querySelector(selector); + ok(button, "we have the \"" + aCategory + "\" button"); + + let firstMenuItem = button.querySelector("menuitem"); + ok(firstMenuItem, "we have the first menu item for the \"" + aCategory + + "\" button"); + + // Turn all the filters off, if they were on. + let menuItem = firstMenuItem; + while (menuItem != null) { + if (menuItem.hasAttribute("prefKey") && isChecked(menuItem)) { + chooseMenuItem(menuItem); + } + menuItem = menuItem.nextSibling; + } + + // Turn all the filters on; make sure the button gets checked. + menuItem = firstMenuItem; + let prefKey; + while (menuItem) { + if (menuItem.hasAttribute("prefKey")) { + prefKey = menuItem.getAttribute("prefKey"); + chooseMenuItem(menuItem); + ok(isChecked(menuItem), "menu item " + prefKey + " for category " + + aCategory + " is checked after clicking it"); + ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "on after clicking the appropriate menu item"); + } + menuItem = menuItem.nextSibling; + } + ok(isChecked(button), "the button for category " + aCategory + " is " + + "checked after turning on all its menu items"); + + // Turn one filter off; make sure the button is still checked. + prefKey = firstMenuItem.getAttribute("prefKey"); + chooseMenuItem(firstMenuItem); + ok(!isChecked(firstMenuItem), "the first menu item for category " + + aCategory + " is no longer checked after clicking it"); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "turned off after clicking the appropriate menu item"); + ok(isChecked(button), "the button for category " + aCategory + " is still " + + "checked after turning off its first menu item"); + + // Turn all the filters off by clicking the main part of the button. + let subbutton = getMainButton(button); + ok(subbutton, "we have the subbutton for category " + aCategory); + + clickButton(subbutton); + ok(!isChecked(button), "the button for category " + aCategory + " is " + + "no longer checked after clicking its main part"); + + menuItem = firstMenuItem; + while (menuItem) { + let prefKey = menuItem.getAttribute("prefKey"); + if (prefKey) { + ok(!isChecked(menuItem), "menu item " + prefKey + " for category " + + aCategory + " is no longer checked after clicking the button"); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "off after clicking the button"); + } + menuItem = menuItem.nextSibling; + } + + // Turn all the filters on by clicking the main part of the button. + clickButton(subbutton); + + ok(isChecked(button), "the button for category " + aCategory + " is " + + "checked after clicking its main part"); + + menuItem = firstMenuItem; + while (menuItem) { + if (menuItem.hasAttribute("prefKey")) { + let prefKey = menuItem.getAttribute("prefKey"); + // The CSS/Log menu item should not be checked. See bug 971798. + if (aCategory == "css" && prefKey == "csslog") { + ok(!isChecked(menuItem), "menu item " + prefKey + " for category " + + aCategory + " should not be checked after clicking the button"); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "off after clicking the button"); + } else { + ok(isChecked(menuItem), "menu item " + prefKey + " for category " + + aCategory + " is checked after clicking the button"); + ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "on after clicking the button"); + } + } + menuItem = menuItem.nextSibling; + } + + // Uncheck the main button by unchecking all the filters + menuItem = firstMenuItem; + while (menuItem) { + // The csslog menu item is already unchecked at this point. + // Make sure it is not selected. See bug 971798. + prefKey = menuItem.getAttribute("prefKey"); + if (prefKey && prefKey != "csslog") { + chooseMenuItem(menuItem); + } + menuItem = menuItem.nextSibling; + } + + ok(!isChecked(button), "the button for category " + aCategory + " is " + + "unchecked after unchecking all its filters"); + + // Turn all the filters on again by clicking the button. + clickButton(subbutton); +} + +function testIsolateFilterButton(aCategory) { + let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]"; + let targetButton = hudBox.querySelector(selector); + ok(targetButton, "we have the \"" + aCategory + "\" button"); + + // Get the main part of the filter button. + let subbutton = getMainButton(targetButton); + ok(subbutton, "we have the subbutton for category " + aCategory); + + // Turn on all the filters by alt clicking the main part of the button. + altClickButton(subbutton); + ok(isChecked(targetButton), "the button for category " + aCategory + + " is checked after isolating for filter"); + + // Check if all the filters for the target button are on. + let menuItems = targetButton.querySelectorAll("menuitem"); + Array.forEach(menuItems, (item) => { + let prefKey = item.getAttribute("prefKey"); + // The CSS/Log filter should not be checked. See bug 971798. + if (aCategory == "css" && prefKey == "csslog") { + ok(!isChecked(item), "menu item " + prefKey + " for category " + + aCategory + " should not be checked after isolating for " + aCategory); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages should be " + + "turned off after isolating for " + aCategory); + } else if (prefKey) { + ok(isChecked(item), "menu item " + prefKey + " for category " + + aCategory + " is checked after isolating for " + aCategory); + ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "turned on after isolating for " + aCategory); + } + }); + + // Ensure all other filter buttons are toggled off and their + // associated filters are turned off + let buttons = hudBox.querySelectorAll(".webconsole-filter-button[category]"); + Array.forEach(buttons, (filterButton) => { + if (filterButton !== targetButton) { + let category = filterButton.getAttribute("category"); + ok(!isChecked(filterButton), "the button for category " + + category + " is unchecked after isolating for " + aCategory); + + menuItems = filterButton.querySelectorAll("menuitem"); + Array.forEach(menuItems, (item) => { + let prefKey = item.getAttribute("prefKey"); + if (prefKey) { + ok(!isChecked(item), "menu item " + prefKey + " for category " + + aCategory + " is unchecked after isolating for " + aCategory); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "turned off after isolating for " + aCategory); + } + }); + + // Turn all the filters on again by clicking the button. + let mainButton = getMainButton(filterButton); + clickButton(mainButton); + } + }); +} + +/** + * Return the main part of the target filter button. + */ +function getMainButton(aTargetButton) { + let anonymousNodes = hud.ui.document.getAnonymousNodes(aTargetButton); + let subbutton; + + for (let i = 0; i < anonymousNodes.length; i++) { + let node = anonymousNodes[i]; + if (node.classList.contains("toolbarbutton-menubutton-button")) { + subbutton = node; + break; + } + } + + return subbutton; +} + +function clickButton(aNode) { + EventUtils.sendMouseEvent({ type: "click" }, aNode); +} + +function altClickButton(aNode) { + EventUtils.sendMouseEvent({ type: "click", altKey: true }, aNode); +} + +function chooseMenuItem(aNode) { + let event = document.createEvent("XULCommandEvent"); + event.initCommandEvent("command", true, true, window, 0, false, false, false, + false, null); + aNode.dispatchEvent(event); +} + +function isChecked(aNode) { + return aNode.getAttribute("checked") === "true"; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js new file mode 100644 index 000000000..ae68ecda8 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js @@ -0,0 +1,177 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +let menuitems = [], menupopups = [], huds = [], tabs = [], runCount = 0; + +const TEST_URI1 = "data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 1"; +const TEST_URI2 = "data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 2"; + +function test() +{ + if (runCount == 0) { + requestLongerTimeout(2); + } + + // open tab 1 + loadTab(TEST_URI1).then((tab) => { + tabs.push(tab.tab); + openConsole().then((hud) => { + hud.iframeWindow.mozRequestAnimationFrame(() => { + info("iframe1 root height " + hud.ui.rootElement.clientHeight); + + // open tab 2 + loadTab(TEST_URI2).then((tab) => { + tabs.push(tab.tab); + openConsole().then((hud) => hud.iframeWindow.mozRequestAnimationFrame(startTest)); + }); + }); + }); + }); +} + +function startTest() +{ + // Find the relevant elements in the Web Console of tab 2. + let win2 = tabs[runCount*2 + 1].linkedBrowser.contentWindow; + huds[1] = HUDService.getHudByWindow(win2); + info("startTest: iframe2 root height " + huds[1].ui.rootElement.clientHeight); + + if (runCount == 0) { + menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodies"); + } + else { + menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodiesContextMenu"); + } + menupopups[1] = menuitems[1].parentNode; + + // Open the context menu from tab 2. + menupopups[1].addEventListener("popupshown", onpopupshown2, false); + executeSoon(function() { + menupopups[1].openPopup(); + }); +} + +function onpopupshown2(aEvent) +{ + menupopups[1].removeEventListener(aEvent.type, onpopupshown2, false); + + // By default bodies are not logged. + isnot(menuitems[1].getAttribute("checked"), "true", + "menuitems[1] is not checked"); + + ok(!huds[1].ui._saveRequestAndResponseBodies, "bodies are not logged"); + + // Enable body logging. + huds[1].ui.setSaveRequestAndResponseBodies(true).then(() => { + menupopups[1].hidePopup(); + }); + + menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) { + menupopups[1].removeEventListener(aEvent.type, _onhidden, false); + + info("menupopups[1] hidden"); + + // Reopen the context menu. + huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2b(aEvent)); + menupopups[1].openPopup(); + }, false); +} + +function testpopup2b(aEvent) { + is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked"); + + menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) { + menupopups[1].removeEventListener(aEvent.type, _onhidden, false); + + info("menupopups[1] hidden"); + + // Switch to tab 1 and open the Web Console context menu from there. + gBrowser.selectedTab = tabs[runCount*2]; + waitForFocus(function() { + // Find the relevant elements in the Web Console of tab 1. + let win1 = tabs[runCount*2].linkedBrowser.contentWindow; + huds[0] = HUDService.getHudByWindow(win1); + + info("iframe1 root height " + huds[0].ui.rootElement.clientHeight); + + menuitems[0] = huds[0].ui.rootElement.querySelector("#saveBodies"); + menupopups[0] = huds[0].ui.rootElement.querySelector("menupopup"); + + menupopups[0].addEventListener("popupshown", onpopupshown1, false); + executeSoon(() => menupopups[0].openPopup()); + }, tabs[runCount*2].linkedBrowser.contentWindow); + }, false); + + executeSoon(function() { + menupopups[1].hidePopup(); + }); +} + +function onpopupshown1(aEvent) +{ + menupopups[0].removeEventListener(aEvent.type, onpopupshown1, false); + + // The menuitem checkbox must not be in sync with the other tabs. + isnot(menuitems[0].getAttribute("checked"), "true", + "menuitems[0] is not checked"); + + // Enable body logging for tab 1 as well. + huds[0].ui.setSaveRequestAndResponseBodies(true).then(() => { + menupopups[0].hidePopup(); + }); + + // Close the menu, and switch back to tab 2. + menupopups[0].addEventListener("popuphidden", function _onhidden(aEvent) { + menupopups[0].removeEventListener(aEvent.type, _onhidden, false); + + info("menupopups[0] hidden"); + + gBrowser.selectedTab = tabs[runCount*2 + 1]; + waitForFocus(function() { + // Reopen the context menu from tab 2. + huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2c(aEvent)); + menupopups[1].openPopup(); + }, tabs[runCount*2 + 1].linkedBrowser.contentWindow); + }, false); +} + +function testpopup2c(aEvent) { + is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked"); + + menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) { + menupopups[1].removeEventListener(aEvent.type, _onhidden, false); + + info("menupopups[1] hidden"); + + // Done if on second run + closeConsole(gBrowser.selectedTab).then(function() { + if (runCount == 0) { + runCount++; + info("start second run"); + executeSoon(test); + } + else { + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = tabs[2]; + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = tabs[1]; + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = tabs[0]; + gBrowser.removeCurrentTab(); + huds = menuitems = menupopups = tabs = null; + executeSoon(finishTest); + } + }); + }, false); + + executeSoon(function() { + menupopups[1].hidePopup(); + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js new file mode 100644 index 000000000..5bc9c3a12 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js @@ -0,0 +1,37 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-603750-websocket.html"; +const TEST_URI2 = "data:text/html;charset=utf-8,Web Console test for bug 603750: Web Socket errors"; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI2); + + let hud = yield openConsole(); + + content.location = TEST_URI; + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "ws://0.0.0.0:81", + source: { url: "test-bug-603750-websocket.js" }, + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "ws://0.0.0.0:82", + source: { url: "test-bug-603750-websocket.js" }, + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + ]}); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js new file mode 100644 index 000000000..cfc1c8c37 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js @@ -0,0 +1,64 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = 'data:text/html;charset=utf-8,<div style="-moz-opacity:0;">test repeated' + + ' css warnings</div><p style="-moz-opacity:0">hi</p>'; +let hud; + +"use strict"; + +/** + * Unit test for bug 611795: + * Repeated CSS messages get collapsed into one. + */ + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + hud.jsterm.clearOutput(true); + + BrowserReload(); + yield loadBrowser(gBrowser.selectedBrowser); + + yield onContentLoaded(); + yield testConsoleLogRepeats(); + + hud = null; +}); + +function onContentLoaded() +{ + let cssWarning = "Unknown property '-moz-opacity'. Declaration dropped."; + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: cssWarning, + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + repeats: 2, + }], + }); +} + +function testConsoleLogRepeats() +{ + let jsterm = hud.jsterm; + + jsterm.clearOutput(); + + jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a line of reasonably long text that I will use to verify that the repeated text node is of an appropriate size.');"); + jsterm.execute(); + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "this is a line of reasonably long text", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 10, + }], + }); +} 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 new file mode 100644 index 000000000..13a1057ec --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js @@ -0,0 +1,29 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html"; + + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + BrowserReload(); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarBug613013", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js new file mode 100644 index 000000000..d35559ab5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js @@ -0,0 +1,80 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613280"; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then((HUD) => { + content.console.log("foobarBazBug613280"); + waitForMessages({ + webconsole: HUD, + messages: [{ + text: "foobarBazBug613280", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(performTest.bind(null, HUD)); + }) + }); +} + +function performTest(HUD, [result]) { + let msg = [...result.matched][0]; + let input = HUD.jsterm.inputNode; + let selection = getSelection(); + let contentSelection = content.getSelection(); + + let clipboard_setup = function() { + goDoCommand("cmd_copy"); + }; + + let clipboard_copy_done = function() { + finishTest(); + }; + + // Check if we first need to clear any existing selections. + if (selection.rangeCount > 0 || contentSelection.rangeCount > 0 || + input.selectionStart != input.selectionEnd) { + if (input.selectionStart != input.selectionEnd) { + input.selectionStart = input.selectionEnd = 0; + } + + if (selection.rangeCount > 0) { + selection.removeAllRanges(); + } + + if (contentSelection.rangeCount > 0) { + contentSelection.removeAllRanges(); + } + + goUpdateCommand("cmd_copy"); + } + + let controller = top.document.commandDispatcher. + getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled"); + + HUD.ui.output.selectMessage(msg); + HUD.outputNode.focus(); + + goUpdateCommand("cmd_copy"); + + controller = top.document.commandDispatcher. + getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled"); + + // Remove new lines since getSelection() includes one between message and line + // number, but the clipboard doesn't (see bug 1119503) + let selectionText = (HUD.iframeWindow.getSelection() + "").replace(/\r?\n|\r/g, " "); + isnot(selectionText.indexOf("foobarBazBug613280"), -1, + "selection text includes 'foobarBazBug613280'"); + + waitForClipboard((str) => { return str.trim() == selectionText.trim(); }, + clipboard_setup, clipboard_copy_done, clipboard_copy_done); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js new file mode 100644 index 000000000..c747ca0a8 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js @@ -0,0 +1,116 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + */ + +let TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613642: remember scroll location"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + let outputNode = hud.outputNode; + let scrollBox = outputNode.parentNode; + + for (let i = 0; i < 150; i++) { + content.console.log("test message " + i); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 149", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + ok(scrollBox.scrollTop > 0, "scroll location is not at the top"); + + // scroll to the first node + outputNode.focus(); + + let scrolled = promise.defer(); + + scrollBox.onscroll = () => { + info("onscroll top " + scrollBox.scrollTop); + if (scrollBox.scrollTop != 0) { + // Wait for scroll to 0. + return; + } + scrollBox.onscroll = null; + is(scrollBox.scrollTop, 0, "scroll location updated (moved to top)"); + scrolled.resolve(); + }; + EventUtils.synthesizeKey("VK_HOME", {}, hud.iframeWindow); + + yield scrolled.promise; + + + // add a message and make sure scroll doesn't change + content.console.log("test message 150"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 150", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + scrolled = promise.defer(); + scrollBox.onscroll = () => { + if (scrollBox.scrollTop != 0) { + // Wait for scroll to stabilize at the top. + return; + } + scrollBox.onscroll = null; + is(scrollBox.scrollTop, 0, "scroll location is still at the top"); + scrolled.resolve(); + }; + + // Make sure that scroll stabilizes at the top. executeSoon() is needed for + // the yield to work. + executeSoon(scrollBox.onscroll); + + yield scrolled.promise; + + // scroll back to the bottom + outputNode.lastChild.focus(); + + scrolled = promise.defer(); + scrollBox.onscroll = () => { + if (scrollBox.scrollTop == 0) { + // Wait for scroll to bottom. + return; + } + scrollBox.onscroll = null; + isnot(scrollBox.scrollTop, 0, "scroll location updated (moved to bottom)"); + scrolled.resolve(); + }; + EventUtils.synthesizeKey("VK_END", {}); + yield scrolled.promise; + + let oldScrollTop = scrollBox.scrollTop; + + content.console.log("test message 151"); + + scrolled = promise.defer(); + scrollBox.onscroll = () => { + if (scrollBox.scrollTop == oldScrollTop) { + // Wait for scroll to change. + return; + } + scrollBox.onscroll = null; + isnot(scrollBox.scrollTop, oldScrollTop, "scroll location updated (moved to bottom again)"); + scrolled.resolve(); + }; + yield scrolled.promise; +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js new file mode 100644 index 000000000..c11ca8878 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js @@ -0,0 +1,81 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613642: maintain scroll with pruning of old messages"; + +let hud; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let outputNode = hud.outputNode; + + Services.prefs.setIntPref("devtools.hud.loglimit.console", 140); + let scrollBoxElement = outputNode.parentNode; + + for (let i = 0; i < 150; i++) { + content.console.log("test message " + i); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 149", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let oldScrollTop = scrollBoxElement.scrollTop; + isnot(oldScrollTop, 0, "scroll location is not at the top"); + + let firstNode = outputNode.firstChild; + ok(firstNode, "found the first message"); + + let msgNode = outputNode.children[80]; + ok(msgNode, "found the 80th message"); + + // scroll to the middle message node + msgNode.scrollIntoView(false); + + isnot(scrollBoxElement.scrollTop, oldScrollTop, + "scroll location updated (scrolled to message)"); + + oldScrollTop = scrollBoxElement.scrollTop; + + // add a message + content.console.log("hello world"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "hello world", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + // Scroll location needs to change, because one message is also removed, and + // we need to scroll a bit towards the top, to keep the current view in sync. + isnot(scrollBoxElement.scrollTop, oldScrollTop, + "scroll location updated (added a message)"); + + isnot(outputNode.firstChild, firstNode, + "first message removed"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.console"); + + hud = null; +}); 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 new file mode 100644 index 000000000..05dcb76fa --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js @@ -0,0 +1,63 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Șucan <mihai.sucan@gmail.com> + */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 614793: jsterm result scroll"; + +"use strict"; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield consoleOpened(hud); +}); + +function consoleOpened(hud) { + let deferred = promise.defer(); + + hud.jsterm.clearOutput(); + + let scrollNode = hud.outputNode.parentNode; + + for (let i = 0; i < 150; i++) { + content.console.log("test message " + i); + } + + let oldScrollTop = -1; + + waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 149", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + oldScrollTop = scrollNode.scrollTop; + isnot(oldScrollTop, 0, "scroll location is not at the top"); + + hud.jsterm.execute("'hello world'").then(onExecute); + }); + + function onExecute(msg) + { + isnot(scrollNode.scrollTop, oldScrollTop, "scroll location updated"); + + oldScrollTop = scrollNode.scrollTop; + + msg.scrollIntoView(false); + + is(scrollNode.scrollTop, oldScrollTop, "scroll location is the same"); + + deferred.resolve(); + } + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js new file mode 100644 index 000000000..cb98e2c0d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js @@ -0,0 +1,29 @@ +/* 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 we report JS exceptions in event handlers coming from +// network requests, like onreadystate for XHR. See bug 618078. + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 618078"; +const TEST_URI2 = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + expectUncaughtException(); + + content.location = TEST_URI2; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug618078exception", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js new file mode 100644 index 000000000..6d3c9e4e5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js @@ -0,0 +1,90 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + BrowserReload(); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }) + + yield performTest(hud, results); +}); + + +function performTest(HUD, results) { + let deferred = promise.defer(); + + let networkMessage = [...results[0].matched][0]; + ok(networkMessage, "network message element"); + + let networkLink = networkMessage.querySelector(".url"); + ok(networkLink, "found network message link"); + + let popupset = document.getElementById("mainPopupSet"); + ok(popupset, "found #mainPopupSet"); + + let popupsShown = 0; + let hiddenPopups = 0; + + let onpopupshown = function() { + document.removeEventListener("popupshown", onpopupshown, false); + popupsShown++; + + executeSoon(function() { + let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]"); + is(popups.length, 1, "found one popup"); + + document.addEventListener("popuphidden", onpopuphidden, false); + + registerCleanupFunction(function() { + is(hiddenPopups, 1, "correct number of popups hidden"); + if (hiddenPopups != 1) { + document.removeEventListener("popuphidden", onpopuphidden, false); + } + }); + + executeSoon(closeConsole); + }); + }; + + let onpopuphidden = function() { + document.removeEventListener("popuphidden", onpopuphidden, false); + hiddenPopups++; + + executeSoon(function() { + let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]"); + is(popups.length, 0, "no popups found"); + + executeSoon(deferred.resolve); + }); + }; + + document.addEventListener("popupshown", onpopupshown, false); + + registerCleanupFunction(function() { + is(popupsShown, 1, "correct number of popups shown"); + if (popupsShown != 1) { + document.removeEventListener("popupshown", onpopupshown, false); + } + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow); + EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow); + EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js new file mode 100644 index 000000000..085887e69 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js @@ -0,0 +1,49 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Sucan <mihai.sucan@gmail.com> + */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield test$(hud); + yield test$$(hud); +}); + + +function* test$(HUD) { + let deferred = promise.defer(); + + HUD.jsterm.clearOutput(); + + HUD.jsterm.execute("$(document.body)", (msg) => { + ok(msg.textContent.indexOf("<p>") > -1, + "jsterm output is correct for $()"); + deferred.resolve(); + }); + + return deferred.promise; +} + +function test$$(HUD) { + let deferred = promise.defer(); + + HUD.jsterm.clearOutput(); + + HUD.jsterm.setInputValue(); + HUD.jsterm.execute("$$(document)", (msg) => { + ok(msg.textContent.indexOf("621644") > -1, + "jsterm output is correct for $$()"); + deferred.resolve(); + }); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js b/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js new file mode 100644 index 000000000..80966a691 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const prefs = { + "net": [ + "network", + "netwarn", + "netxhr", + "networkinfo" + ], + "css": [ + "csserror", + "cssparser", + "csslog" + ], + "js": [ + "exception", + "jswarn", + "jslog", + ], + "logging": [ + "error", + "warn", + "info", + "log" + ] +}; + +let test = asyncTest(function* () { + // Set all prefs to true + for (let category in prefs) { + prefs[category].forEach(function(pref) { + Services.prefs.setBoolPref("devtools.webconsole.filter." + pref, true); + }); + } + + yield loadTab("about:blank"); + + let hud = yield openConsole(); + + let hud2 = yield onConsoleOpen(hud); + let hud3 = yield onConsoleReopen1(hud2); + yield onConsoleReopen2(hud3); + + // Clear prefs + for (let category in prefs) { + prefs[category].forEach(function(pref) { + Services.prefs.clearUserPref("devtools.webconsole.filter." + pref); + }); + } +}); + +function onConsoleOpen(hud) { + let deferred = promise.defer(); + + let hudBox = hud.ui.rootElement; + + // Check if the filters menuitems exists and are checked + for (let category in prefs) { + let button = hudBox.querySelector(".webconsole-filter-button[category=\"" + + category + "\"]"); + ok(isChecked(button), "main button for " + category + " category is checked"); + + prefs[category].forEach(function(pref) { + let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]"); + ok(isChecked(menuitem), "menuitem for " + pref + " is checked"); + }); + } + + // Set all prefs to false + for (let category in prefs) { + prefs[category].forEach(function(pref) { + hud.setFilterState(pref, false); + }); + } + + //Re-init the console + closeConsole().then(() => { + openConsole().then(deferred.resolve); + }); + + return deferred.promise; +} + +function onConsoleReopen1(hud) { + info("testing after reopening once"); + let deferred = promise.defer(); + + let hudBox = hud.ui.rootElement; + + // Check if the filter button and menuitems are unchecked + for (let category in prefs) { + let button = hudBox.querySelector(".webconsole-filter-button[category=\"" + + category + "\"]"); + ok(isUnchecked(button), "main button for " + category + " category is not checked"); + + prefs[category].forEach(function(pref) { + let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]"); + ok(isUnchecked(menuitem), "menuitem for " + pref + " is not checked"); + }); + } + + // Set first pref in each category to true + for (let category in prefs) { + hud.setFilterState(prefs[category][0], true); + } + + // Re-init the console + closeConsole().then(() => { + openConsole().then(deferred.resolve); + }); + + return deferred.promise; +} + +function onConsoleReopen2(hud) { + info("testing after reopening again"); + + let hudBox = hud.ui.rootElement; + + // Check the main category button is checked and first menuitem is checked + for (let category in prefs) { + let button = hudBox.querySelector(".webconsole-filter-button[category=\"" + + category + "\"]"); + ok(isChecked(button), category + " button is checked when first pref is true"); + + let pref = prefs[category][0]; + let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]"); + ok(isChecked(menuitem), "first " + category + " menuitem is checked"); + } +} + +function isChecked(aNode) { + return aNode.getAttribute("checked") === "true"; +} + +function isUnchecked(aNode) { + return aNode.getAttribute("checked") === "false"; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js b/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js new file mode 100644 index 000000000..84cd52c4a --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for https://bugzilla.mozilla.org/show_bug.cgi?id=623749 +// Map Control + A to Select All, In the web console input, on Windows + +const TEST_URI = "data:text/html;charset=utf-8,Test console for bug 623749"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + jsterm.setInputValue("Ignore These Four Words"); + let inputNode = jsterm.inputNode; + + // Test select all with Control + A. + EventUtils.synthesizeKey("a", { ctrlKey: true }); + let inputLength = inputNode.selectionEnd - inputNode.selectionStart; + is(inputLength, inputNode.value.length, "Select all of input"); + + // Test do nothing on Control + E. + jsterm.setInputValue("Ignore These Four Words"); + inputNode.selectionStart = 0; + EventUtils.synthesizeKey("e", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "Control + E does not move to end of input"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js new file mode 100644 index 000000000..88f771275 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js @@ -0,0 +1,130 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Mihai Sucan <mihai.sucan@gmail.com> + */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for bug 630733"; +const TEST_URI2 = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs"; + +let lastFinishedRequests = {}; +let webConsoleClient; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield consoleOpened(hud); + yield getHeaders(); + yield getContent(); + + performTest(); +}); + +function consoleOpened(hud) +{ + let deferred = promise.defer(); + + webConsoleClient = hud.ui.webConsoleClient; + hud.ui.setSaveRequestAndResponseBodies(true).then(() => { + ok(hud.ui._saveRequestAndResponseBodies, + "The saveRequestAndResponseBodies property was successfully set."); + + HUDService.lastFinishedRequest.callback = (aHttpRequest) => { + let status = aHttpRequest.response.status; + lastFinishedRequests[status] = aHttpRequest; + if ("301" in lastFinishedRequests && + "404" in lastFinishedRequests) { + deferred.resolve(); + } + } + content.location = TEST_URI2; + }); + + return deferred.promise; +} + +function getHeaders() +{ + let deferred = promise.defer(); + + HUDService.lastFinishedRequest.callback = null; + + ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently"); + ok("404" in lastFinishedRequests, "request 2: 404 Not found"); + + webConsoleClient.getResponseHeaders(lastFinishedRequests["301"].actor, + function (aResponse) { + lastFinishedRequests["301"].response.headers = aResponse.headers; + + webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor, + function (aResponse) { + lastFinishedRequests["404"].response.headers = aResponse.headers; + executeSoon(deferred.resolve); + }); + }); + return deferred.promise; +} + +function getContent() +{ + let deferred = promise.defer(); + + webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor, + function (aResponse) { + lastFinishedRequests["301"].response.content = aResponse.content; + lastFinishedRequests["301"].discardResponseBody = aResponse.contentDiscarded; + + webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor, + function (aResponse) { + lastFinishedRequests["404"].response.content = aResponse.content; + lastFinishedRequests["404"].discardResponseBody = + aResponse.contentDiscarded; + + webConsoleClient = null; + executeSoon(deferred.resolve); + }); + }); + return deferred.promise; +} + +function performTest() +{ + function readHeader(aName) + { + for (let header of headers) { + if (header.name == aName) { + return header.value; + } + } + return null; + } + + let headers = lastFinishedRequests["301"].response.headers; + is(readHeader("Content-Type"), "text/html", + "we do have the Content-Type header"); + is(readHeader("Content-Length"), 71, "Content-Length is correct"); + is(readHeader("Location"), "/redirect-from-bug-630733", + "Content-Length is correct"); + is(readHeader("x-foobar-bug630733"), "bazbaz", + "X-Foobar-bug630733 is correct"); + + let body = lastFinishedRequests["301"].response.content; + ok(!body.text, "body discarded for request 1"); + ok(lastFinishedRequests["301"].discardResponseBody, + "body discarded for request 1 (confirmed)"); + + headers = lastFinishedRequests["404"].response.headers; + ok(!readHeader("Location"), "no Location header"); + ok(!readHeader("x-foobar-bug630733"), "no X-Foobar-bug630733 header"); + + body = lastFinishedRequests["404"].response.content.text; + isnot(body.indexOf("404"), -1, + "body is correct for request 2"); + + lastFinishedRequests = webConsoleClient = null; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js new file mode 100644 index 000000000..3ee086810 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html"; + +let getterValue = null; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(hud) { + let doc = content.wrappedJSObject.document; + getterValue = doc.foobar._val; + hud.jsterm.execute("console.dir(document)"); + + let onOpen = onViewOpened.bind(null, hud); + hud.jsterm.once("variablesview-fetched", onOpen); +} + +function onViewOpened(hud, event, view) +{ + let doc = content.wrappedJSObject.document; + + findVariableViewProperties(view, [ + { name: /^(width|height)$/, dontMatch: 1 }, + { name: "foobar._val", value: getterValue }, + { name: "foobar.val", isGetter: true }, + ], { webconsole: hud }).then(function() { + is(doc.foobar._val, getterValue, "getter did not execute"); + is(doc.foobar.val, getterValue+1, "getter executed"); + is(doc.foobar._val, getterValue+1, "getter executed (recheck)"); + + let textContent = hud.outputNode.textContent; + is(textContent.indexOf("document.body.client"), -1, + "no document.width/height warning displayed"); + + getterValue = null; + finishTest(); + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js new file mode 100644 index 000000000..0410a6968 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -0,0 +1,81 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html"; + +function test() { + requestLongerTimeout(6); + + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(HUD) { + let tools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; + let JSPropertyProvider = tools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider; + + let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + tmp.addDebuggerToGlobal(tmp); + let dbg = new tmp.Debugger; + + let jsterm = HUD.jsterm; + let win = content.wrappedJSObject; + let dbgWindow = dbg.makeGlobalObjectReference(win); + let container = win._container; + + // Make sure autocomplete does not walk through iterators and generators. + let result = container.gen1.next(); + let completion = JSPropertyProvider(dbgWindow, null, "_container.gen1."); + isnot(completion.matches.length, 0, "Got matches for gen1"); + + is(result+1, container.gen1.next(), "gen1.next() did not execute"); + + result = container.gen2.next(); + + completion = JSPropertyProvider(dbgWindow, null, "_container.gen2."); + isnot(completion.matches.length, 0, "Got matches for gen2"); + + is((result/2+1)*2, container.gen2.next(), + "gen2.next() did not execute"); + + result = container.iter1.next(); + is(result[0], "foo", "iter1.next() [0] is correct"); + is(result[1], "bar", "iter1.next() [1] is correct"); + + completion = JSPropertyProvider(dbgWindow, null, "_container.iter1."); + isnot(completion.matches.length, 0, "Got matches for iter1"); + + result = container.iter1.next(); + is(result[0], "baz", "iter1.next() [0] is correct"); + is(result[1], "baaz", "iter1.next() [1] is correct"); + + let dbgContent = dbg.makeGlobalObjectReference(content); + completion = JSPropertyProvider(dbgContent, null, "_container.iter2."); + isnot(completion.matches.length, 0, "Got matches for iter2"); + + completion = JSPropertyProvider(dbgWindow, null, "window._container."); + ok(completion, "matches available for window._container"); + ok(completion.matches.length, "matches available for window (length)"); + + jsterm.clearOutput(); + + jsterm.execute("window._container", (msg) => { + jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD)); + let anchor = msg.querySelector(".message-body a"); + EventUtils.synthesizeMouse(anchor, 2, 2, {}, HUD.iframeWindow); + }); +} + +function testVariablesView(aWebconsole, aEvent, aView) { + findVariableViewProperties(aView, [ + { name: "gen1", isGenerator: true }, + { name: "gen2", isGenerator: true }, + { name: "iter1", isIterator: true }, + { name: "iter2", isIterator: true }, + ], { webconsole: aWebconsole }).then(function() { + executeSoon(finishTest); + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js new file mode 100644 index 000000000..d6dc79bbb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js @@ -0,0 +1,241 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that network log messages bring up the network panel. + +const TEST_NETWORK_REQUEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-network-request.html"; + +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; + +const TEST_DATA_JSON_CONTENT = + '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console network logging tests"; + +let lastRequest = null; +let requestCallback = null; +let hud, browser; + +function test() +{ + const PREF = "devtools.webconsole.persistlog"; + const NET_PREF = "devtools.webconsole.filter.networkinfo"; + const NETXHR_PREF = "devtools.webconsole.filter.netxhr" + const MIXED_AC_PREF = "security.mixed_content.block_active_content" + let original = Services.prefs.getBoolPref(NET_PREF); + let originalXhr = Services.prefs.getBoolPref(NETXHR_PREF); + let originalMixedActive = Services.prefs.getBoolPref(MIXED_AC_PREF); + Services.prefs.setBoolPref(NET_PREF, true); + Services.prefs.setBoolPref(NETXHR_PREF, true); + Services.prefs.setBoolPref(MIXED_AC_PREF, false); + Services.prefs.setBoolPref(PREF, true); + registerCleanupFunction(() => { + Services.prefs.setBoolPref(NET_PREF, original); + Services.prefs.setBoolPref(NETXHR_PREF, originalXhr); + Services.prefs.setBoolPref(MIXED_AC_PREF, originalMixedActive); + Services.prefs.clearUserPref(PREF); + }); + + loadTab(TEST_URI).then((tab) => { + browser = tab.browser; + openConsole().then((aHud) => { + hud = aHud; + + HUDService.lastFinishedRequest.callback = function(aRequest) { + lastRequest = aRequest; + if (requestCallback) { + requestCallback(); + } + }; + + executeSoon(testPageLoad); + }) + }); +} + +function testPageLoad() +{ + requestCallback = function() { + // Check if page load was logged correctly. + ok(lastRequest, "Page load was logged"); + is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI, + "Logged network entry is page load"); + is(lastRequest.request.method, "GET", "Method is correct"); + lastRequest = null; + requestCallback = null; + executeSoon(testPageLoadBody); + }; + + content.location = TEST_NETWORK_REQUEST_URI; +} + +function testPageLoadBody() +{ + let loaded = false; + let requestCallbackInvoked = false; + + // Turn off logging of request bodies and check again. + requestCallback = function() { + ok(lastRequest, "Page load was logged again"); + lastRequest = null; + requestCallback = null; + requestCallbackInvoked = true; + + if (loaded) { + executeSoon(testXhrGet); + } + }; + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + loaded = true; + + if (requestCallbackInvoked) { + executeSoon(testXhrGet); + } + }, true); + + content.location.reload(); +} + +function testXhrGet() +{ + requestCallback = function() { + ok(lastRequest, "testXhrGet() was logged"); + is(lastRequest.request.method, "GET", "Method is correct"); + lastRequest = null; + requestCallback = null; + executeSoon(testXhrWarn); + }; + + // Start the XMLHttpRequest() GET test. + content.wrappedJSObject.testXhrGet(); +} + +function testXhrWarn() +{ + requestCallback = function() { + ok(lastRequest, "testXhrWarn() was logged"); + is(lastRequest.request.method, "GET", "Method is correct"); + lastRequest = null; + requestCallback = null; + executeSoon(testXhrPost); + }; + + // Start the XMLHttpRequest() warn test. + content.wrappedJSObject.testXhrWarn(); +} + +function testXhrPost() +{ + requestCallback = function() { + ok(lastRequest, "testXhrPost() was logged"); + is(lastRequest.request.method, "POST", "Method is correct"); + lastRequest = null; + requestCallback = null; + executeSoon(testFormSubmission); + }; + + // Start the XMLHttpRequest() POST test. + content.wrappedJSObject.testXhrPost(); +} + +function testFormSubmission() +{ + // Start the form submission test. As the form is submitted, the page is + // loaded again. Bind to the load event to catch when this is done. + requestCallback = function() { + ok(lastRequest, "testFormSubmission() was logged"); + is(lastRequest.request.method, "POST", "Method is correct"); + + // 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, + }, + { + text: "test-data.json", + category: CATEGORY_NETWORK, + severity: SEVERITY_INFO, + isXhr: true, + count: 2, + }, + { + text: "http://example.com/", + category: CATEGORY_NETWORK, + severity: SEVERITY_WARNING, + isXhr: true, + count: 1, + }, + ], + }).then(testLiveFilteringOnSearchStrings); + }; + + let form = content.document.querySelector("form"); + ok(form, "we have the HTML form"); + form.submit(); +} + +function testLiveFilteringOnSearchStrings() { + setStringFilter("http"); + isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " + + "search string is set to \"http\""); + + setStringFilter("HTTP"); + isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " + + "search string is set to \"HTTP\""); + + setStringFilter("hxxp"); + is(countMessageNodes(), 0, "the log nodes are hidden when the search " + + "string is set to \"hxxp\""); + + setStringFilter("ht tp"); + isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " + + "search string is set to \"ht tp\""); + + setStringFilter(""); + isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " + + "search string is removed"); + + setStringFilter("json"); + is(countMessageNodes(), 2, "the log nodes show only the nodes with \"json\""); + + setStringFilter("'foo'"); + is(countMessageNodes(), 0, "the log nodes are hidden when searching for " + + "the string 'foo'"); + + setStringFilter("foo\"bar'baz\"boo'"); + is(countMessageNodes(), 0, "the log nodes are hidden when searching for " + + "the string \"foo\"bar'baz\"boo'\""); + + HUDService.lastFinishedRequest.callback = null; + lastRequest = null; + requestCallback = null; + hud = browser = null; + finishTest(); +} + +function countMessageNodes() { + let messageNodes = hud.outputNode.querySelectorAll(".message"); + let displayedMessageNodes = 0; + let view = hud.iframeWindow; + for (let i = 0; i < messageNodes.length; i++) { + let computedStyle = view.getComputedStyle(messageNodes[i], null); + if (computedStyle.display !== "none") + displayedMessageNodes++; + } + + return displayedMessageNodes; +} + +function setStringFilter(aValue) +{ + hud.ui.filterBox.value = aValue; + hud.ui.adjustVisibilityOnSearchStringChange(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js b/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js new file mode 100644 index 000000000..e269b3e99 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js @@ -0,0 +1,80 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that the Web Console limits the number of lines displayed according to +// the user's preferences. + +const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642108."; +const LOG_LIMIT = 20; + +function test() { + let hud; + + Task.spawn(runner).then(finishTest); + + function* runner() { + let {tab} = yield loadTab(TEST_URI); + + Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", LOG_LIMIT); + Services.prefs.setBoolPref("devtools.webconsole.filter.cssparser", true); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser"); + Services.prefs.clearUserPref("devtools.webconsole.filter.cssparser"); + }); + + hud = yield openConsole(tab); + + for (let i = 0; i < 5; i++) { + logCSSMessage("css log x"); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "css log x", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + repeats: 5, + }], + }); + + for (let i = 0; i < LOG_LIMIT + 5; i++) { + logCSSMessage("css log " + i); + } + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "css log 5", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + { + text: "css log 24", // LOG_LIMIT + 5 + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }], + }); + + is(hud.ui.outputNode.querySelectorAll(".message").length, LOG_LIMIT, + "number of messages"); + + is(Object.keys(hud.ui._repeatNodes).length, LOG_LIMIT, + "repeated nodes pruned from repeatNodes"); + + let msg = [...result.matched][0]; + let repeats = msg.querySelector(".message-repeats"); + is(repeats.getAttribute("value"), 1, + "repeated nodes pruned from repeatNodes (confirmed)"); + } + + function logCSSMessage(msg) { + let node = hud.ui.createMessageNode(CATEGORY_CSS, SEVERITY_WARNING, msg); + hud.ui.outputMessage(CATEGORY_CSS, node); + } +} 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 new file mode 100644 index 000000000..ac489ee42 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js @@ -0,0 +1,225 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the Web Console limits the number of lines displayed according to +// the limit set for each category. + +const INIT_URI = "data:text/html;charset=utf-8,Web Console test for bug 644419: Console should " + + "have user-settable log limits for each message category"; + +const TEST_URI = "http://example.com/browser/browser/devtools/" + + "webconsole/test/test-bug-644419-log-limits.html"; + +let hud, outputNode; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(INIT_URI); + + hud = yield openConsole(); + + hud.jsterm.clearOutput(); + outputNode = hud.outputNode; + + let loaded = loadBrowser(browser); + + expectUncaughtException(); + + content.location = TEST_URI; + yield loaded; + + yield testWebDevLimits(); + yield testWebDevLimits2(); + yield testJsLimits(); + yield testJsLimits2(); + + yield testNetLimits(); + yield loadImage(); + yield testCssLimits(); + yield testCssLimits2(); + + hud = outputNode = null; +}); + +function testWebDevLimits() { + Services.prefs.setIntPref("devtools.hud.loglimit.console", 10); + + // Find the sentinel entry. + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "bar is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }) +} + +function testWebDevLimits2() { + // Fill the log with Web Developer errors. + for (let i = 0; i < 11; i++) { + content.console.log("test message " + i); + } + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 10", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + 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"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.console"); + }); +} + +function testJsLimits() { + Services.prefs.setIntPref("devtools.hud.loglimit.exception", 10); + + hud.jsterm.clearOutput(); + content.console.log("testing JS limits"); + + // Find the sentinel entry. + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "testing JS limits", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +function testJsLimits2() { + // Fill the log with JS errors. + let head = content.document.getElementsByTagName("head")[0]; + for (let i = 0; i < 11; i++) { + var script = content.document.createElement("script"); + script.text = "fubar" + i + ".bogus(6);"; + + expectUncaughtException(); + head.insertBefore(script, head.firstChild); + } + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "fubar10 is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }).then(() => { + testLogEntry(outputNode, "fubar0 is not defined", "first message is pruned", false, true); + findLogEntry("fubar1 is not defined"); + // Check if the sentinel entry is still there. + findLogEntry("testing JS limits"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.exception"); + }); +} + +var gCounter, gImage; + +function testNetLimits() { + Services.prefs.setIntPref("devtools.hud.loglimit.network", 10); + + hud.jsterm.clearOutput(); + content.console.log("testing Net limits"); + + // Find the sentinel entry. + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "testing Net limits", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + // Fill the log with network messages. + gCounter = 0; + }); +} + +function loadImage() { + if (gCounter < 11) { + let body = content.document.getElementsByTagName("body")[0]; + gImage && gImage.removeEventListener("load", loadImage, true); + gImage = content.document.createElement("img"); + gImage.src = "test-image.png?_fubar=" + gCounter; + body.insertBefore(gImage, body.firstChild); + gImage.addEventListener("load", loadImage, true); + gCounter++; + return; + } + + is(gCounter, 11, "loaded 11 files"); + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "test-image.png", + url: "test-image.png?_fubar=10", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }).then(() => { + let msgs = outputNode.querySelectorAll(".message[category=network]"); + is(msgs.length, 10, "number of network messages"); + isnot(msgs[0].url.indexOf("fubar=1"), -1, "first network message"); + isnot(msgs[1].url.indexOf("fubar=2"), -1, "second network message"); + findLogEntry("testing Net limits"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.network"); + }); +} + +function testCssLimits() { + Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", 10); + + hud.jsterm.clearOutput(); + content.console.log("testing CSS limits"); + + // Find the sentinel entry. + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "testing CSS limits", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +function testCssLimits2() { + // Fill the log with CSS errors. + let body = content.document.getElementsByTagName("body")[0]; + for (let i = 0; i < 11; i++) { + var div = content.document.createElement("div"); + div.setAttribute("style", "-moz-foobar" + i + ": 42;"); + body.insertBefore(div, body.firstChild); + } + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "-moz-foobar10", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }], + }).then(() => { + testLogEntry(outputNode, "Unknown property '-moz-foobar0'", + "first message is pruned", false, true); + findLogEntry("Unknown property '-moz-foobar1'"); + // Check if the sentinel entry is still there. + findLogEntry("testing CSS limits"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser"); + }); +} 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 new file mode 100644 index 000000000..39be46e41 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js @@ -0,0 +1,54 @@ +/* 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 logging methods display the method location along with +// the output in the console. + +const TEST_URI = "data:text/html;charset=utf-8,Web Console file location display test"; +const TEST_URI2 = "http://example.com/browser/browser/devtools/" + + "webconsole/test/" + + "test-bug-646025-console-file-location.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + content.location = TEST_URI2; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "message for level log", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + source: { url: "test-file-location.js", line: 5 }, + }, + { + text: "message for level info", + category: CATEGORY_WEBDEV, + severity: SEVERITY_INFO, + source: { url: "test-file-location.js", line: 6 }, + }, + { + text: "message for level warn", + category: CATEGORY_WEBDEV, + severity: SEVERITY_WARNING, + source: { url: "test-file-location.js", line: 7 }, + }, + { + text: "message for level error", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + source: { url: "test-file-location.js", line: 8 }, + }, + { + text: "message for level debug", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + source: { url: "test-file-location.js", line: 9 }, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js new file mode 100644 index 000000000..9f2969214 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -0,0 +1,109 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that document.body autocompletes in the web console. +const TEST_URI = "data:text/html;charset=utf-8,Web Console autocompletion bug in document.body"; + +"use strict"; + +let gHUD; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + gHUD = yield openConsole(); + + yield consoleOpened(); + yield autocompletePopupHidden(); + let view = yield testPropertyPanel(); + yield onVariablesViewReady(view); + + gHUD = null; +}); + +function consoleOpened(aHud) { + let deferred = promise.defer(); + + let jsterm = gHUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + + ok(!popup.isOpen, "popup is not open"); + + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, jsterm._autocompleteCache.length, + "popup.itemCount is correct"); + isnot(jsterm._autocompleteCache.indexOf("addEventListener"), -1, + "addEventListener is in the list of suggestions"); + isnot(jsterm._autocompleteCache.indexOf("bgColor"), -1, + "bgColor is in the list of suggestions"); + isnot(jsterm._autocompleteCache.indexOf("ATTRIBUTE_NODE"), -1, + "ATTRIBUTE_NODE is in the list of suggestions"); + + popup._panel.addEventListener("popuphidden", deferred.resolve, false); + + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }, false); + + jsterm.setInputValue("document.body"); + EventUtils.synthesizeKey(".", {}); + + return deferred.promise; +} + +function autocompletePopupHidden() +{ + let deferred = promise.defer(); + + let jsterm = gHUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + let inputNode = jsterm.inputNode; + + popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false); + + ok(!popup.isOpen, "popup is not open"); + + jsterm.once("autocomplete-updated", function() { + is(completeNode.value, testStr + "dy", "autocomplete shows document.body"); + deferred.resolve(); + }); + + let inputStr = "document.b"; + jsterm.setInputValue(inputStr); + EventUtils.synthesizeKey("o", {}); + let testStr = inputStr.replace(/./g, " ") + " "; + + return deferred.promise; +} + +function testPropertyPanel() +{ + let deferred = promise.defer(); + + let jsterm = gHUD.jsterm; + jsterm.clearOutput(); + jsterm.execute("document", (msg) => { + jsterm.once("variablesview-fetched", (aEvent, aView) => { + deferred.resolve(aView); + }); + let anchor = msg.querySelector(".message-body a"); + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow); + }); + + return deferred.promise; +} + +function onVariablesViewReady(aView) +{ + return findVariableViewProperties(aView, [ + { name: "body", value: "<body>" }, + ], { webconsole: gHUD }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js new file mode 100644 index 000000000..a5191c831 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js @@ -0,0 +1,107 @@ +/* vim: set ft=javascript 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/. */ + +// Tests that the $0 console helper works as intended. + +let inspector, h1, outputNode; + +function createDocument() { + let doc = content.document; + let div = doc.createElement("div"); + h1 = doc.createElement("h1"); + let p1 = doc.createElement("p"); + let p2 = doc.createElement("p"); + let div2 = doc.createElement("div"); + let p3 = doc.createElement("p"); + doc.title = "Inspector Tree Selection Test"; + h1.textContent = "Inspector Tree Selection Test"; + p1.textContent = "This is some example text"; + p2.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " + + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + + "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + p3.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " + + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + + "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + div.appendChild(h1); + div.appendChild(p1); + div.appendChild(p2); + div2.appendChild(p3); + doc.body.appendChild(div); + doc.body.appendChild(div2); + setupHighlighterTests(); +} + +function setupHighlighterTests() { + ok(h1, "we have the header node"); + openInspector().then(runSelectionTests); +} + +let runSelectionTests = Task.async(function*(aInspector) { + inspector = aInspector; + + let onPickerStarted = inspector.toolbox.once("picker-started"); + inspector.toolbox.highlighterUtils.startPicker(); + yield onPickerStarted; + + info("Picker mode started, now clicking on H1 to select that node"); + h1.scrollIntoView(); + let onPickerStopped = inspector.toolbox.once("picker-stopped"); + let onInspectorUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(h1, {}, content); + yield onPickerStopped; + yield onInspectorUpdated; + + info("Picker mode stopped, H1 selected, now switching to the console"); + let hud = yield openConsole(gBrowser.selectedTab); + + performWebConsoleTests(hud); +}); + +function performWebConsoleTests(hud) { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let jsterm = hud.jsterm; + outputNode = hud.outputNode; + + jsterm.clearOutput(); + jsterm.execute("$0", onNodeOutput); + + function onNodeOutput(node) { + isnot(node.textContent.indexOf("<h1>"), -1, "correct output for $0"); + + jsterm.clearOutput(); + jsterm.execute("$0.textContent = 'bug653531'", onNodeUpdate); + } + + function onNodeUpdate(node) { + isnot(node.textContent.indexOf("bug653531"), -1, + "correct output for $0.textContent"); + is(inspector.selection.node.textContent, "bug653531", + "node successfully updated"); + + inspector = h1 = outputNode = null; + gBrowser.removeCurrentTab(); + finishTest(); + } +} + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html;charset=utf-8,test for highlighter helper in web console"; +} 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 new file mode 100644 index 000000000..8cb87324e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js @@ -0,0 +1,66 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the Console API implements the time() and timeEnd() methods. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" + + "test/test-bug-658368-time-methods.html"; + +const TEST_URI2 = "data:text/html;charset=utf-8,<script>" + + "console.timeEnd('bTimer');</script>"; + +const TEST_URI3 = "data:text/html;charset=utf-8,<script>" + + "console.time('bTimer');</script>"; + +const TEST_URI4 = "data:text/html;charset=utf-8," + + "<script>console.timeEnd('bTimer');</script>"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud1 = yield openConsole(); + + yield waitForMessages({ + webconsole: hud1, + messages: [{ + name: "aTimer started", + consoleTime: "aTimer", + }, { + name: "aTimer end", + consoleTimeEnd: "aTimer", + }], + }); + + // The next test makes sure that timers with the same name but in separate + // tabs, do not contain the same value. + let { browser } = yield loadTab(TEST_URI2); + let hud2 = yield openConsole(); + + testLogEntry(hud2.outputNode, "bTimer: timer started", + "bTimer was not started", false, true); + + // The next test makes sure that timers with the same name but in separate + // pages, do not contain the same value. + content.location = TEST_URI3; + + yield waitForMessages({ + webconsole: hud2, + messages: [{ + name: "bTimer started", + consoleTime: "bTimer", + }], + }); + + hud2.jsterm.clearOutput(); + + // Now the following console.timeEnd() call shouldn't display anything, + // if the timers in different pages are not related. + content.location = TEST_URI4; + yield loadBrowser(browser); + + testLogEntry(hud2.outputNode, "bTimer: timer started", + "bTimer was not started", false, true); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js new file mode 100644 index 000000000..1569899a1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js @@ -0,0 +1,28 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that console.dir works as intended. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 659907: " + + "Expand console object with a dir method" + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + hud.jsterm.execute("console.dir(document)"); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + + yield findVariableViewProperties(varView, [ + { name: "__proto__.__proto__.querySelectorAll", value: "querySelectorAll()" }, + { name: "location", value: /Location \u2192 data:Web/ }, + { name: "__proto__.write", value: "write()" }, + ], { webconsole: hud }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js b/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js new file mode 100644 index 000000000..2f057e4ca --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_660806_history_nav.js @@ -0,0 +1,51 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 660806 - history navigation must not show the autocomplete popup"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield consoleOpened(hud); +}); + +function consoleOpened(HUD) +{ + let deferred = promise.defer(); + + let jsterm = HUD.jsterm; + let popup = jsterm.autocompletePopup; + let onShown = function() { + ok(false, "popup shown"); + }; + + jsterm.execute("window.foobarBug660806 = {\ + 'location': 'value0',\ + 'locationbar': 'value1'\ + }"); + + popup._panel.addEventListener("popupshown", onShown, false); + + ok(!popup.isOpen, "popup is not open"); + + ok(!jsterm.lastInputValue, "no lastInputValue"); + jsterm.setInputValue("window.foobarBug660806.location"); + is(jsterm.lastInputValue, "window.foobarBug660806.location", + "lastInputValue is correct"); + + EventUtils.synthesizeKey("VK_RETURN", {}); + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.lastInputValue, "window.foobarBug660806.location", + "lastInputValue is correct, again"); + + executeSoon(function() { + ok(!popup.isOpen, "popup is not open"); + popup._panel.removeEventListener("popupshown", onShown, false); + executeSoon(deferred.resolve); + }); + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js b/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js new file mode 100644 index 000000000..ff719877c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js @@ -0,0 +1,77 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that console.group/groupEnd works as intended. +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 664131: Expand console object with group methods"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + let jsterm = hud.jsterm; + let outputNode = hud.outputNode; + + hud.jsterm.clearOutput(); + + yield jsterm.execute("console.group('bug664131a')") + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug664131a", + consoleGroup: 1, + }], + }); + + yield jsterm.execute("console.log('bug664131a-inside')") + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug664131a-inside", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + groupDepth: 1, + }], + }); + + yield jsterm.execute('console.groupEnd("bug664131a")'); + yield jsterm.execute('console.log("bug664131-outside")'); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug664131-outside", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + groupDepth: 0, + }], + }); + + yield jsterm.execute('console.groupCollapsed("bug664131b")'); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug664131b", + consoleGroup: 1, + }], + }); + + // Test that clearing the console removes the indentation. + hud.jsterm.clearOutput(); + yield jsterm.execute('console.log("bug664131-cleared")'); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug664131-cleared", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + groupDepth: 0, + }], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js new file mode 100644 index 000000000..d2e45489d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js @@ -0,0 +1,60 @@ +/* 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 the autocompletion results contain the names of JSTerm helpers. + +const TEST_URI = "data:text/html;charset=utf8,<p>test JSTerm Helpers autocomplete"; + +let jsterm; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + jsterm = hud.jsterm; + let input = jsterm.inputNode; + let popup = jsterm.autocompletePopup; + + // Test if 'i' gives 'inspect' + input.value = "i"; + input.setSelectionRange(1, 1); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + let newItems = popup.getItems().map(function(e) {return e.label;}); + ok(newItems.indexOf("inspect") > -1, "autocomplete results contain helper 'inspect'"); + + // Test if 'window.' does not give 'inspect'. + input.value = "window."; + input.setSelectionRange(7, 7); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems().map(function(e) {return e.label;}); + is(newItems.indexOf("inspect"), -1, "autocomplete results do not contain helper 'inspect'"); + + // Test if 'dump(i' gives 'inspect' + input.value = "dump(i"; + input.setSelectionRange(6, 6); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems().map(function(e) {return e.label;}); + ok(newItems.indexOf("inspect") > -1, "autocomplete results contain helper 'inspect'"); + + // Test if 'window.dump(i' gives 'inspect' + input.value = "window.dump(i"; + input.setSelectionRange(13, 13); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems().map(function(e) {return e.label;}); + ok(newItems.indexOf("inspect") > -1, "autocomplete results contain helper 'inspect'"); + + jsterm = null; +}); + +function complete(type) { + let updated = jsterm.once("autocomplete-updated"); + jsterm.complete(type); + return updated; +}
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js b/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js new file mode 100644 index 000000000..14d4aeeab --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_704295.js @@ -0,0 +1,39 @@ +/* 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 for bug 704295 + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + testCompletion(hud); +}); + +function testCompletion(hud) { + var jsterm = hud.jsterm; + var input = jsterm.inputNode; + + // Test typing 'var d = 5;' and press RETURN + jsterm.setInputValue("var d = "); + EventUtils.synthesizeKey("5", {}); + EventUtils.synthesizeKey(";", {}); + is(input.value, "var d = 5;", "var d = 5;"); + is(jsterm.completeNode.value, "", "no completion"); + EventUtils.synthesizeKey("VK_RETURN", {}); + is(jsterm.completeNode.value, "", "clear completion on execute()"); + + // Test typing 'var a = d' and press RETURN + jsterm.setInputValue("var a = "); + EventUtils.synthesizeKey("d", {}); + is(input.value, "var a = d", "var a = d"); + is(jsterm.completeNode.value, "", "no completion"); + EventUtils.synthesizeKey("VK_RETURN", {}); + is(jsterm.completeNode.value, "", "clear completion on execute()"); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js b/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js new file mode 100644 index 000000000..9aff77a44 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js @@ -0,0 +1,32 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/browser/test-console.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + var jsterm = hud.jsterm; + var input = jsterm.inputNode; + + is(input.getAttribute("focused"), "true", "input has focus"); + EventUtils.synthesizeKey("VK_TAB", {}); + is(input.getAttribute("focused"), "", "focus moved away"); + + // Test user changed something + input.focus(); + EventUtils.synthesizeKey("A", {}); + EventUtils.synthesizeKey("VK_TAB", {}); + is(input.getAttribute("focused"), "true", "input is still focused"); + + // Test non empty input but not changed since last focus + input.blur(); + input.focus(); + EventUtils.synthesizeKey("VK_RIGHT", {}); + EventUtils.synthesizeKey("VK_TAB", {}); + is(input.getAttribute("focused"), "", "input moved away"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js new file mode 100644 index 000000000..bc2f70b37 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js @@ -0,0 +1,60 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Tanvi Vyas <tanvi@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that the Web Console Mixed Content messages are displayed + +const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test"; +const TEST_HTTPS_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/MixedContent"; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref("security.mixed_content.block_display_content", false); + Services.prefs.setBoolPref("security.mixed_content.block_active_content", false); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield testMixedContent(hud); + + Services.prefs.clearUserPref("security.mixed_content.block_display_content"); + Services.prefs.clearUserPref("security.mixed_content.block_active_content"); +}); + +let testMixedContent = Task.async(function*(hud) { + content.location = TEST_HTTPS_URI; + + let results = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "example.com", + category: CATEGORY_NETWORK, + severity: SEVERITY_WARNING, + }], + }); + + let msg = [...results[0].matched][0]; + ok(msg, "page load logged"); + ok(msg.classList.contains("mixed-content"), ".mixed-content element"); + + let link = msg.querySelector(".learn-more-link"); + ok(link, "mixed content link element"); + is(link.textContent, "[Mixed Content]", "link text is accurate"); + + yield simulateMessageLinkClick(link, LEARN_MORE_URI); + + ok(!msg.classList.contains("filtered-by-type"), "message is not filtered"); + + hud.setFilterState("netwarn", false); + + ok(msg.classList.contains("filtered-by-type"), "message is filtered"); + + hud.setFilterState("netwarn", true); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js b/browser/devtools/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js new file mode 100644 index 000000000..fac6f2cc2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that warnings about ineffective iframe sandboxing are logged to the +// web console when necessary (and not otherwise). + +const TEST_URI_WARNING = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html"; +const TEST_URI_NOWARNING = [ + "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html", + "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html", + "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html", + "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html", + "http://example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html" +]; + +const INEFFECTIVE_IFRAME_SANDBOXING_MSG = "An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can remove its sandboxing."; +const SENTINEL_MSG = "testing ineffective sandboxing message"; + +function test() +{ + loadTab(TEST_URI_WARNING).then(() => { + openConsole().then((hud) => { + content.console.log(SENTINEL_MSG) + waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Ineffective iframe sandboxing warning displayed successfully", + text: INEFFECTIVE_IFRAME_SANDBOXING_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING + }, + { + text: SENTINEL_MSG, + severity: SEVERITY_LOG + } + ] + }).then(() => { + let msgs = hud.outputNode.querySelectorAll(".message[category=security]"); + is(msgs.length, 1, "one security message"); + testNoWarning(0); + }); + }) + }); +} + +function testNoWarning(id) +{ + loadTab(TEST_URI_NOWARNING[id]).then(() => { + openConsole().then((hud) => { + content.console.log(SENTINEL_MSG) + waitForMessages({ + webconsole: hud, + messages: [ + { + text: SENTINEL_MSG, + severity: SEVERITY_LOG + } + ] + }).then(() => { + let msgs = hud.outputNode.querySelectorAll(".message[category=security]"); + is(msgs.length, 0, "no security messages (case " + id + ")"); + + id += 1; + if (id < TEST_URI_NOWARNING.length) { + testNoWarning(id); + } else { + finishTest(); + } + }); + }) + }); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js new file mode 100644 index 000000000..d0322f6e9 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* + * Tests that errors about insecure passwords are logged + * to the web console + */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html"; +const INSECURE_PASSWORD_MSG = "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."; + + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Insecure password error displayed successfully", + text: INSECURE_PASSWORD_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING + }, + ], + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js new file mode 100644 index 000000000..428add7d2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* + * Tests that errors about insecure passwords are logged + * to the web console + */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html"; +const INSECURE_PASSWORD_MSG = "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."; +const INSECURE_FORM_ACTION_MSG = "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."; +const INSECURE_IFRAME_MSG = "Password fields present on an insecure (http://) iframe. This is a security risk that allows user login credentials to be stolen."; +const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let result = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Insecure password error displayed successfully", + text: INSECURE_PASSWORD_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING + }, + { + name: "Insecure iframe error displayed successfully", + text: INSECURE_IFRAME_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING + }, + { + name: "Insecure form action error displayed successfully", + text: INSECURE_FORM_ACTION_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING + }, + ], + }); + + yield testClickOpenNewTab(hud, result); +}); + +function testClickOpenNewTab(hud, [result]) { + let msg = [...result.matched][0]; + let warningNode = msg.querySelector(".learn-more-link"); + ok(warningNode, "learn more link"); + return simulateMessageLinkClick(warningNode, INSECURE_PASSWORDS_URI); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js b/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js new file mode 100644 index 000000000..2517c8e94 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js @@ -0,0 +1,137 @@ +/* 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/. */ + +// This is a test for the Open URL context menu item +// that is shown for network requests +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html" +const COMMAND_NAME = "consoleCmd_openURL"; +const CONTEXT_MENU_ID = "#menu_openURL"; + +let HUD = null, outputNode = null, contextMenu = null; + +let test = asyncTest(function* () { + Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true); + + yield loadTab(TEST_URI); + HUD = yield openConsole(); + + let results = yield consoleOpened(); + yield onConsoleMessage(results); + + let results2 = yield testOnNetActivity(); + let msg = yield onNetworkMessage(results2); + + yield testOnNetActivity_contextmenu(msg); + + Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo"); + + HUD = null, outputNode = null, contextMenu = null; +}); + +function consoleOpened() { + outputNode = HUD.outputNode; + contextMenu = HUD.iframeWindow.document.getElementById("output-contextmenu"); + + HUD.jsterm.clearOutput(); + + content.console.log("bug 764572"); + + return waitForMessages({ + webconsole: HUD, + messages: [{ + text: "bug 764572", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +function onConsoleMessage(aResults) { + outputNode.focus(); + outputNode.selectedItem = [...aResults[0].matched][0]; + + // Check if the command is disabled non-network messages. + goUpdateCommand(COMMAND_NAME); + let controller = top.document.commandDispatcher + .getControllerForCommand(COMMAND_NAME); + + let isDisabled = !controller || !controller.isCommandEnabled(COMMAND_NAME); + ok(isDisabled, COMMAND_NAME + " should be disabled."); + + outputNode.selectedItem.scrollIntoView(); + return waitForContextMenu(contextMenu, outputNode.selectedItem, () => { + let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden; + ok(isHidden, CONTEXT_MENU_ID + " should be hidden."); + }); +} + +function testOnNetActivity() { + HUD.jsterm.clearOutput(); + + // Reload the url to show net activity in console. + content.location.reload(); + + return waitForMessages({ + webconsole: HUD, + messages: [{ + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); +} + +function onNetworkMessage(aResults) { + let deferred = promise.defer(); + + outputNode.focus(); + let msg = [...aResults[0].matched][0]; + ok(msg, "network message"); + HUD.ui.output.selectMessage(msg); + + let currentTab = gBrowser.selectedTab; + let newTab = null; + + gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(aEvent) { + gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true); + newTab = aEvent.target; + newTab.linkedBrowser.addEventListener("load", onTabLoaded, true); + }, true); + + function onTabLoaded() { + newTab.linkedBrowser.removeEventListener("load", onTabLoaded, true); + gBrowser.removeTab(newTab); + gBrowser.selectedTab = currentTab; + executeSoon(deferred.resolve.bind(null, msg)); + } + + // Check if the command is enabled for a network message. + goUpdateCommand(COMMAND_NAME); + let controller = top.document.commandDispatcher + .getControllerForCommand(COMMAND_NAME); + ok(controller.isCommandEnabled(COMMAND_NAME), + COMMAND_NAME + " should be enabled."); + + // Try to open the URL. + goDoCommand(COMMAND_NAME); + + return deferred.promise; +} + +function testOnNetActivity_contextmenu(msg) { + let deferred = promise.defer(); + + outputNode.focus(); + HUD.ui.output.selectMessage(msg); + msg.scrollIntoView(); + + info("net activity context menu"); + + waitForContextMenu(contextMenu, msg, () => { + let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden; + ok(isShown, CONTEXT_MENU_ID + " should be shown."); + }).then(deferred.resolve); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js b/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js new file mode 100644 index 000000000..05580a7c3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js @@ -0,0 +1,78 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that message source links for js errors and console API calls open in +// the jsdebugger when clicked. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" + + "/test-bug-766001-js-console-links.html"; + +function test() { + let hud; + + requestLongerTimeout(2); + Task.spawn(runner).then(finishTest); + + function* runner() { + expectUncaughtException(); + + let {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + + let [exceptionRule, consoleRule] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "document.bar", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "Blah Blah", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let exceptionMsg = [...exceptionRule.matched][0]; + let consoleMsg = [...consoleRule.matched][0]; + let nodes = [exceptionMsg.querySelector(".message-location"), + consoleMsg.querySelector(".message-location")]; + ok(nodes[0], ".location node for the exception message"); + ok(nodes[1], ".location node for the console message"); + + for (let i = 0; i < nodes.length; i++) { + yield checkClickOnNode(i, nodes[i]); + yield gDevTools.showToolbox(hud.target, "webconsole"); + } + + // check again the first node. + yield checkClickOnNode(0, nodes[0]); + } + + function* checkClickOnNode(index, node) { + info("checking click on node index " + index); + + let url = node.getAttribute("title"); + ok(url, "source url found for index " + index); + + let line = node.sourceLine; + ok(line, "found source line for index " + index); + + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "click" }, node); + }); + + yield hud.ui.once("source-in-debugger-opened"); + + let toolbox = yield gDevTools.getToolbox(hud.target); + let {panelWin: { DebuggerView: view }} = toolbox.getPanel("jsdebugger"); + is(view.Sources.selectedValue, + getSourceActor(view.Sources, url), + "expected source url"); + is(view.editor.getCursor().line, line - 1, "expected source line"); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js new file mode 100644 index 000000000..6c0400492 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js @@ -0,0 +1,31 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that the Web Console CSP messages are displayed + +const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test"; +const TEST_VIOLATION = "https://example.com/browser/browser/devtools/webconsole/test/test_bug_770099_violation.html"; +const CSP_VIOLATION_MSG = 'Content Security Policy: The page\'s settings blocked the loading of a resource at http://some.example.com/test.png ("default-src https://example.com").' + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + content.location = TEST_VIOLATION; + yield loaded; + + yield waitForSuccess({ + name: "CSP policy URI warning displayed successfully", + validator: function() { + return hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1; + } + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js new file mode 100644 index 000000000..6cc5172c5 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js @@ -0,0 +1,147 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" + + "/test-bug-782653-css-errors.html"; + +let nodes, hud, StyleEditorUI; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + + let styleEditor = yield testViewSource(); + yield onStyleEditorReady(styleEditor); + + nodes = hud = StyleEditorUI = null; +}); + +function testViewSource() +{ + let deferred = promise.defer(); + + waitForMessages({ + webconsole: hud, + messages: [{ + text: "'font-weight'", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + { + text: "'color'", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }], + }).then(([error1Rule, error2Rule]) => { + let error1Msg = [...error1Rule.matched][0]; + let error2Msg = [...error2Rule.matched][0]; + nodes = [error1Msg.querySelector(".message-location"), + error2Msg.querySelector(".message-location")]; + ok(nodes[0], ".message-location node for the first error"); + ok(nodes[1], ".message-location node for the second error"); + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + toolbox.once("styleeditor-selected", (event, panel) => { + StyleEditorUI = panel.UI; + + let count = 0; + StyleEditorUI.on("editor-added", function() { + if (++count == 2) { + deferred.resolve(panel); + } + }); + }); + + EventUtils.sendMouseEvent({ type: "click" }, nodes[0]); + }); + + return deferred.promise; +} + +function onStyleEditorReady(aPanel) +{ + let deferred = promise.defer(); + + let win = aPanel.panelWindow; + ok(win, "Style Editor Window is defined"); + ok(StyleEditorUI, "Style Editor UI is defined"); + + waitForFocus(function() { + info("style editor window focused"); + + let href = nodes[0].getAttribute("title"); + let line = nodes[0].sourceLine; + ok(line, "found source line"); + + checkStyleEditorForSheetAndLine(href, line - 1).then(function() { + info("first check done"); + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + let href = nodes[1].getAttribute("title"); + let line = nodes[1].sourceLine; + ok(line, "found source line"); + + toolbox.selectTool("webconsole").then(function() { + info("webconsole selected"); + + toolbox.once("styleeditor-selected", function(aEvent) { + info(aEvent + " event fired"); + + checkStyleEditorForSheetAndLine(href, line - 1).then(deferred.resolve); + }); + + EventUtils.sendMouseEvent({ type: "click" }, nodes[1]); + }); + }); + }, win); + + return deferred.promise; +} + +function checkStyleEditorForSheetAndLine(aHref, aLine) +{ + let foundEditor = null; + for (let editor of StyleEditorUI.editors) { + if (editor.styleSheet.href == aHref) { + foundEditor = editor; + break; + } + } + + ok(foundEditor, "found style editor for " + aHref); + return performLineCheck(foundEditor, aLine); +} + +function performLineCheck(aEditor, aLine) +{ + let deferred = promise.defer(); + + function checkForCorrectState() + { + is(aEditor.sourceEditor.getCursor().line, aLine, + "correct line is selected"); + is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex, + "correct stylesheet is selected in the editor"); + + executeSoon(deferred.resolve); + } + + info("wait for source editor to load"); + + // Get out of the styleeditor-selected event loop. + executeSoon(() => { + aEditor.getSourceEditor().then(() => { + // Get out of the editor's source-editor-load event loop. + executeSoon(checkForCorrectState); + }); + }); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js b/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js new file mode 100644 index 000000000..f6a66572f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js @@ -0,0 +1,217 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * zmgmoz <zmgmoz@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n +// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845 +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 804845 and bug 619598"; + +let jsterm, inputNode; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + doTests(hud); + + jsterm = inputNode = null; +}); + +function doTests(HUD) { + jsterm = HUD.jsterm; + inputNode = jsterm.inputNode; + ok(!jsterm.inputNode.value, "inputNode.value is empty"); + is(jsterm.inputNode.selectionStart, 0); + is(jsterm.inputNode.selectionEnd, 0); + + testSingleLineInputNavNoHistory(); + testMultiLineInputNavNoHistory(); + testNavWithHistory(); +} + +function testSingleLineInputNavNoHistory() { + // Single char input + EventUtils.synthesizeKey("1", {}); + is(inputNode.selectionStart, 1, "caret location after single char input"); + + // nav to start/end with ctrl-a and ctrl-e; + EventUtils.synthesizeKey("a", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "caret location after single char input and ctrl-a"); + + EventUtils.synthesizeKey("e", { ctrlKey: true }); + is(inputNode.selectionStart, 1, "caret location after single char input and ctrl-e"); + + // Second char input + EventUtils.synthesizeKey("2", {}); + // nav to start/end with up/down keys; verify behaviour using ctrl-p/ctrl-n + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.selectionStart, 0, "caret location after two char input and VK_UP"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.selectionStart, 2, "caret location after two char input and VK_DOWN"); + + EventUtils.synthesizeKey("a", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "move caret to beginning of 2 char input with ctrl-a"); + EventUtils.synthesizeKey("a", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "no change of caret location on repeat ctrl-a"); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "no change of caret location on ctrl-p from beginning of line"); + + EventUtils.synthesizeKey("e", { ctrlKey: true }); + is(inputNode.selectionStart, 2, "move caret to end of 2 char input with ctrl-e"); + EventUtils.synthesizeKey("e", { ctrlKey: true }); + is(inputNode.selectionStart, 2, "no change of caret location on repeat ctrl-e"); + EventUtils.synthesizeKey("n", { ctrlKey: true }); + is(inputNode.selectionStart, 2, "no change of caret location on ctrl-n from end of line"); + + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "ctrl-p moves to start of line"); + + EventUtils.synthesizeKey("n", { ctrlKey: true }); + is(inputNode.selectionStart, 2, "ctrl-n moves to end of line"); +} + +function testMultiLineInputNavNoHistory() { + let lineValues = ["one", "2", "something longer", "", "", "three!"]; + jsterm.setInputValue(""); + // simulate shift-return + for (let i = 0; i < lineValues.length; i++) { + jsterm.setInputValue(inputNode.value + lineValues[i]); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }); + } + let inputValue = inputNode.value; + is(inputNode.selectionStart, inputNode.selectionEnd); + is(inputNode.selectionStart, inputValue.length, "caret at end of multiline input"); + + // possibility newline is represented by one ('\r', '\n') or two ('\r\n') chars + let newlineString = inputValue.match(/(\r\n?|\n\r?)$/)[0]; + + // Ok, test navigating within the multi-line string! + EventUtils.synthesizeKey("VK_UP", {}); + let expectedStringAfterCarat = lineValues[5]+newlineString; + is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat, + "up arrow from end of multiline"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.value.slice(inputNode.selectionStart), "", + "down arrow from within multiline"); + + // navigate up through input lines + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat, + "ctrl-p from end of multiline"); + + for (let i = 4; i >= 0; i--) { + EventUtils.synthesizeKey("p", { ctrlKey: true }); + expectedStringAfterCarat = lineValues[i] + newlineString + expectedStringAfterCarat; + is(inputNode.value.slice(inputNode.selectionStart), expectedStringAfterCarat, + "ctrl-p from within line " + i + " of multiline input"); + } + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, 0, "reached start of input"); + is(inputNode.value, inputValue, + "no change to multiline input on ctrl-p from beginning of multiline"); + + // navigate to end of first line + EventUtils.synthesizeKey("e", { ctrlKey: true }); + let caretPos = inputNode.selectionStart; + let expectedStringBeforeCarat = lineValues[0]; + is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat, + "ctrl-e into multiline input"); + EventUtils.synthesizeKey("e", { ctrlKey: true }); + is(inputNode.selectionStart, caretPos, + "repeat ctrl-e doesn't change caret position in multiline input"); + + // navigate down one line; ctrl-a to the beginning; ctrl-e to end + for (let i = 1; i < lineValues.length; i++) { + EventUtils.synthesizeKey("n", { ctrlKey: true }); + EventUtils.synthesizeKey("a", { ctrlKey: true }); + caretPos = inputNode.selectionStart; + expectedStringBeforeCarat += newlineString; + is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat, + "ctrl-a to beginning of line " + (i+1) + " in multiline input"); + + EventUtils.synthesizeKey("e", { ctrlKey: true }); + caretPos = inputNode.selectionStart; + expectedStringBeforeCarat += lineValues[i]; + is(inputNode.value.slice(0, caretPos), expectedStringBeforeCarat, + "ctrl-e to end of line " + (i+1) + "in multiline input"); + } +} + +function testNavWithHistory() { + // NOTE: Tests does NOT currently define behaviour for ctrl-p/ctrl-n with + // caret placed _within_ single line input + let values = ['"single line input"', + '"a longer single-line input to check caret repositioning"', + ['"multi-line"', '"input"', '"here!"'].join("\n"), + ]; + // submit to history + for (let i = 0; i < values.length; i++) { + jsterm.setInputValue(values[i]); + jsterm.execute(); + } + is(inputNode.selectionStart, 0, "caret location at start of empty line"); + + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, values[values.length-1].length, + "caret location correct at end of last history input"); + + // Navigate backwards history with ctrl-p + for (let i = values.length-1; i > 0; i--) { + let match = values[i].match(/(\n)/g); + if (match) { + // multi-line inputs won't update from history unless caret at beginning + EventUtils.synthesizeKey("a", { ctrlKey: true }); + for (let i = 0; i < match.length; i++) { + EventUtils.synthesizeKey("p", { ctrlKey: true }); + } + EventUtils.synthesizeKey("p", { ctrlKey: true }); + } else { + // single-line inputs will update from history from end of line + EventUtils.synthesizeKey("p", { ctrlKey: true }); + } + is(inputNode.value, values[i-1], + "ctrl-p updates inputNode from backwards history values[" + i-1 + "]"); + } + let inputValue = inputNode.value; + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, 0, + "ctrl-p at beginning of history moves caret location to beginning of line"); + is(inputNode.value, inputValue, + "no change to input value on ctrl-p from beginning of line"); + + // Navigate forwards history with ctrl-n + for (let i = 1; i<values.length; i++) { + EventUtils.synthesizeKey("n", { ctrlKey: true }); + is(inputNode.value, values[i], + "ctrl-n updates inputNode from forwards history values[" + i + "]"); + is(inputNode.selectionStart, values[i].length, + "caret location correct at end of history input for values[" + i + "]"); + } + EventUtils.synthesizeKey("n", { ctrlKey: true }); + ok(!inputNode.value, "ctrl-n at end of history updates to empty input"); + + // Simulate editing multi-line + inputValue = "one\nlinebreak"; + jsterm.setInputValue(inputValue); + + // Attempt nav within input + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.value, inputValue, + "ctrl-p from end of multi-line does not trigger history"); + + EventUtils.synthesizeKey("a", { ctrlKey: true }); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.value, values[values.length-1], + "ctrl-p from start of multi-line triggers history"); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js b/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js new file mode 100644 index 000000000..98633c327 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js @@ -0,0 +1,63 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * zmgmoz <zmgmoz@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Test that user input that is not submitted in the command line input is not +// lost after navigating in history. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=817834 + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 817834"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + testEditedInputHistory(hud); +}); + +function testEditedInputHistory(HUD) { + let jsterm = HUD.jsterm; + let inputNode = jsterm.inputNode; + ok(!inputNode.value, "inputNode.value is empty"); + is(inputNode.selectionStart, 0); + is(inputNode.selectionEnd, 0); + + jsterm.setInputValue('"first item"'); + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.value, '"first item"', "null test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.value, '"first item"', "null test history down"); + + jsterm.execute(); + is(inputNode.value, "", "cleared input line after submit"); + + jsterm.setInputValue('"editing input 1"'); + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.value, '"first item"', "test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.value, '"editing input 1"', + "test history down restores in-progress input"); + + jsterm.setInputValue('"second item"'); + jsterm.execute(); + jsterm.setInputValue('"editing input 2"'); + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.value, '"second item"', "test history up"); + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.value, '"first item"', "test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.value, '"second item"', "test history down"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(inputNode.value, '"editing input 2"', + "test history down restores new in-progress input again"); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js b/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js new file mode 100644 index 000000000..c5d8b22fb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_837351_securityerrors.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-837351-security-errors.html"; + +let test = asyncTest(function* () { + yield pushPrefEnv(); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let button = hud.ui.rootElement.querySelector(".webconsole-filter-button[category=\"security\"]"); + ok(button, "Found security button in the web console"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Logged blocking mixed active content", + text: "Blocked loading mixed active content \"http://example.com/\"", + category: CATEGORY_SECURITY, + severity: SEVERITY_ERROR + }, + ], + }); +}); + +function pushPrefEnv() +{ + let deferred = promise.defer(); + let options = {'set': [["security.mixed_content.block_active_content", true]]}; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_846918_hsts_invalid-headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_846918_hsts_invalid-headers.js new file mode 100644 index 000000000..a3911f4b4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_846918_hsts_invalid-headers.js @@ -0,0 +1,35 @@ + /* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Tests that errors about invalid HSTS security headers are logged + * to the web console */ +const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html"; +const HSTS_INVALID_HEADER_MSG = "The site specified an invalid Strict-Transport-Security header."; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Invalid HSTS header error displayed successfully", + text: HSTS_INVALID_HEADER_MSG, + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + ], + }); + + yield testClickOpenNewTab(hud, results); +}); + +function testClickOpenNewTab(hud, results) { + let warningNode = results[0].clickableElements[0]; + ok(warningNode, "link element"); + ok(warningNode.classList.contains("learn-more-link"), "link class name"); + return simulateMessageLinkClick(warningNode, LEARN_MORE_URI); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js b/browser/devtools/webconsole/test/browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js new file mode 100644 index 000000000..56b566654 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js @@ -0,0 +1,113 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the 'Log Request and Response Bodies' buttons can be toggled with keyboard. +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 915141: Toggle log response bodies with keyboard"; +let hud; + +function test() { + let saveBodiesMenuItem; + let saveBodiesContextMenuItem; + + loadTab(TEST_URI).then(({tab: tab}) => { + return openConsole(tab); + }) + .then((aHud) => { + hud = aHud; + saveBodiesMenuItem = hud.ui.rootElement.querySelector("#saveBodies"); + saveBodiesContextMenuItem = hud.ui.rootElement.querySelector("#saveBodiesContextMenu"); + + // Test the context menu action. + info("Testing 'Log Request and Response Bodies' menuitem of right click context menu."); + + return openPopup(saveBodiesContextMenuItem); + }) + .then(() => { + is(saveBodiesContextMenuItem.getAttribute("checked"), "false", + "Context menu: 'log responses' is not checked before action."); + is(hud.ui._saveRequestAndResponseBodies, false, + "Context menu: Responses are not logged before action."); + + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + + return waitForUpdate(saveBodiesContextMenuItem); + }) + .then(() => { + is(saveBodiesContextMenuItem.getAttribute("checked"), "true", + "Context menu: 'log responses' is checked after menuitem was selected with keyboard."); + is(hud.ui._saveRequestAndResponseBodies, true, + "Context menu: Responses are saved after menuitem was selected with keyboard."); + + return openPopup(saveBodiesMenuItem); + }) + .then(() => { + // Test the 'Net' menu item. + info("Testing 'Log Request and Response Bodies' menuitem of 'Net' menu in the console."); + // 'Log Request and Response Bodies' should be selected due to previous test. + + is(saveBodiesMenuItem.getAttribute("checked"), "true", + "Console net menu: 'log responses' is checked before action."); + is(hud.ui._saveRequestAndResponseBodies, true, + "Console net menu: Responses are logged before action."); + + // The correct item is the last one in the menu. + EventUtils.synthesizeKey("VK_UP", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + + return waitForUpdate(saveBodiesMenuItem); + }) + .then(() => { + is(saveBodiesMenuItem.getAttribute("checked"), "false", + "Console net menu: 'log responses' is NOT checked after menuitem was selected with keyboard."); + is(hud.ui._saveRequestAndResponseBodies, false, + "Responses are NOT saved after menuitem was selected with keyboard."); + hud = null; + }) + .then(finishTest); +} + +/** + * Opens and waits for the menu containing aMenuItem to open. + * @param aMenuItem MenuItem + * A MenuItem in a menu that should be opened. + * @return A promise that's resolved once menu is open. + */ +function openPopup(aMenuItem) { + let menu = aMenuItem.parentNode; + + let menuOpened = promise.defer(); + let uiUpdated = promise.defer(); + // The checkbox menuitem is updated asynchronously on 'popupshowing' event so + // it's better to wait for both the update to happen and the menu to open + // before continuing or the test might fail due to a race between menu being + // shown and the item updated to have the correct state. + hud.ui.once("save-bodies-ui-toggled", uiUpdated.resolve); + menu.addEventListener("popupshown", function onPopup () { + menu.removeEventListener("popupshown", onPopup); + menuOpened.resolve(); + }); + + menu.openPopup(); + return Promise.all([menuOpened.promise, uiUpdated.promise]); +} + +/** + * Waits for the settings and menu containing aMenuItem to update. + * @param aMenuItem MenuItem + * The menuitem that should be updated. + * @return A promise that's resolved once the settings and menus are updated. + */ +function waitForUpdate(aMenuItem) { + info("Waiting for settings update to complete."); + let deferred = promise.defer(); + hud.ui.once("save-bodies-pref-reversed", function () { + hud.ui.once("save-bodies-ui-toggled", deferred.resolve); + // The checked state is only updated once the popup is shown. + aMenuItem.parentNode.openPopup(); + }); + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_cached_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_cached_autocomplete.js new file mode 100644 index 000000000..1c780a3fc --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_cached_autocomplete.js @@ -0,0 +1,108 @@ +/* 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 the cached autocomplete results are used when the new +// user input is a subset of the existing completion results. + +const TEST_URI = "data:text/html;charset=utf8,<p>test cached autocompletion results"; + +let jsterm; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + jsterm = hud.jsterm; + let input = jsterm.inputNode; + let popup = jsterm.autocompletePopup; + + // Test if 'doc' gives 'document' + input.value = "doc"; + input.setSelectionRange(3, 3); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(input.value, "doc", "'docu' completion (input.value)"); + is(jsterm.completeNode.value, " ument", "'docu' completion (completeNode)"); + + // Test typing 'window.'. + input.value = "window."; + input.setSelectionRange(7, 7); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + ok(popup.getItems().length > 0, "'window.' gave a list of suggestions") + + yield jsterm.execute("window.docfoobar = true"); + + // Test typing 'window.doc'. + input.value = "window.doc"; + input.setSelectionRange(10, 10); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + let newItems = popup.getItems(); + ok(newItems.every(function(item) { + return item.label != "docfoobar"; + }), "autocomplete cached results do not contain docfoobar. list has not been updated"); + + // Test that backspace does not cause a request to the server + input.value = "window.do"; + input.setSelectionRange(9, 9); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems(); + ok(newItems.every(function(item) { + return item.label != "docfoobar"; + }), "autocomplete cached results do not contain docfoobar. list has not been updated"); + + yield jsterm.execute("delete window.docfoobar"); + + // Test if 'window.getC' gives 'getComputedStyle' + input.value = "window." + input.setSelectionRange(7, 7); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + input.value = "window.getC"; + input.setSelectionRange(11, 11); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems(); + ok(!newItems.every(function(item) { + return item.label != "getComputedStyle"; + }), "autocomplete results do contain getComputedStyle"); + + // Test if 'dump(d' gives non-zero results + input.value = "dump(d"; + input.setSelectionRange(6, 6); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + ok(popup.getItems().length > 0, "'dump(d' gives non-zero results"); + + // Test that 'dump(window.)' works. + input.value = "dump(window.)"; + input.setSelectionRange(12, 12); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + yield jsterm.execute("window.docfoobar = true"); + + // Make sure 'dump(window.doc)' does not contain 'docfoobar'. + input.value = "dump(window.doc)"; + input.setSelectionRange(15, 15); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + newItems = popup.getItems(); + ok(newItems.every(function(item) { + return item.label != "docfoobar"; + }), "autocomplete cached results do not contain docfoobar. list has not been updated"); + + yield jsterm.execute("delete window.docfoobar"); + + jsterm = null; +}); + +function complete(type) { + let updated = jsterm.once("autocomplete-updated"); + jsterm.complete(type); + return updated; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js new file mode 100644 index 000000000..bc919a258 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the cd() jsterm helper function works as expected. See bug 609872. + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html"; + + const parentMessages = [{ + name: "document.title in parent iframe", + text: "bug 609872 - iframe parent", + category: CATEGORY_OUTPUT, + }, { + name: "paragraph content", + text: "p: test for bug 609872 - iframe parent", + category: CATEGORY_OUTPUT, + }, { + name: "object content", + text: "obj: parent!", + category: CATEGORY_OUTPUT, + }]; + + const childMessages = [{ + name: "document.title in child iframe", + text: "bug 609872 - iframe child", + category: CATEGORY_OUTPUT, + }, { + name: "paragraph content", + text: "p: test for bug 609872 - iframe child", + category: CATEGORY_OUTPUT, + }, { + name: "object content", + text: "obj: child!", + category: CATEGORY_OUTPUT, + }]; + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + + yield executeWindowTest(); + + yield waitForMessages({ webconsole: hud, messages: parentMessages }); + + info("cd() into the iframe using a selector"); + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd('iframe')"); + yield executeWindowTest(); + + yield waitForMessages({ webconsole: hud, messages: childMessages }); + + info("cd() out of the iframe, reset to default window"); + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd()"); + yield executeWindowTest(); + + yield waitForMessages({ webconsole: hud, messages: parentMessages }); + + info("call cd() with unexpected arguments"); + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd(document)"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Cannot cd()", + category: CATEGORY_OUTPUT, + severity: SEVERITY_ERROR, + }], + }); + + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd('p')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Cannot cd()", + category: CATEGORY_OUTPUT, + severity: SEVERITY_ERROR, + }], + }); + + info("cd() into the iframe using an iframe DOM element"); + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd($('iframe'))"); + yield executeWindowTest(); + + yield waitForMessages({ webconsole: hud, messages: childMessages }); + + info("cd(window.parent)"); + hud.jsterm.clearOutput(); + yield hud.jsterm.execute("cd(window.parent)"); + yield executeWindowTest(); + + yield waitForMessages({ webconsole: hud, messages: parentMessages }); + + yield closeConsole(tab); + } + + function executeWindowTest() { + yield hud.jsterm.execute("document.title"); + yield hud.jsterm.execute("'p: ' + document.querySelector('p').textContent"); + yield hud.jsterm.execute("'obj: ' + window.foobarBug609872"); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_certificate_messages.js b/browser/devtools/webconsole/test/browser_webconsole_certificate_messages.js new file mode 100644 index 000000000..8a90876c0 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_certificate_messages.js @@ -0,0 +1,94 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the Web Console shows weak crypto warnings (SHA-1 Certificate, SSLv3, and RC4) + +const TEST_URI = "data:text/html;charset=utf8,Web Console weak crypto warnings test"; +const TEST_URI_PATH = "/browser/browser/devtools/webconsole/test/test-certificate-messages.html"; + +let gWebconsoleTests = [ + {url: "https://sha1ee.example.com" + TEST_URI_PATH, + name: "SHA1 warning displayed successfully", + warning: ["SHA-1"], nowarning: ["SSL 3.0", "RC4"]}, + {url: "https://ssl3.example.com" + TEST_URI_PATH, + name: "SSL3 warning displayed successfully", + pref: [["security.tls.version.min", 0]], + warning: ["SSL 3.0"], nowarning: ["SHA-1", "RC4"]}, + {url: "https://rc4.example.com" + TEST_URI_PATH, + name: "RC4 warning displayed successfully", + pref: [["security.tls.insecure_fallback_hosts", "rc4.example.com"]], + warning: ["RC4"], nowarning: ["SHA-1", "SSL 3.0"]}, + {url: "https://rc4.example.com" + TEST_URI_PATH + "?", + name: "Unrestricted RC4 fallback worked", + pref: [["security.tls.unrestricted_rc4_fallback", true]], + warning: ["RC4"], nowarning: ["SHA-1", "SSL 3.0"]}, + {url: "https://ssl3rc4.example.com" + TEST_URI_PATH, + name: "SSL3 and RC4 warning displayed successfully", + pref: [["security.tls.version.min", 0], + ["security.tls.insecure_fallback_hosts", "ssl3rc4.example.com"]], + warning: ["SSL 3.0", "RC4"], nowarning: ["SHA-1"]}, + {url: "https://sha256ee.example.com" + TEST_URI_PATH, + name: "SSL warnings appropriately not present", + warning: [], nowarning: ["SHA-1", "SSL 3.0", "RC4"]}, +]; +const TRIGGER_MSG = "If you haven't seen ssl warnings yet, you won't"; + +let gHud = undefined, gContentBrowser; +let gCurrentTest; + +function test() { + registerCleanupFunction(function () { + gHud = gContentBrowser = null; + }); + + loadTab(TEST_URI).then(({browser}) => { + gContentBrowser = browser; + openConsole().then(runTestLoop); + }); +} + +function runTestLoop(theHud) { + gCurrentTest = gWebconsoleTests.shift(); + if (!gCurrentTest) { + finishTest(); + return; + } + if (!gHud) { + gHud = theHud; + } + gHud.jsterm.clearOutput(); + gContentBrowser.addEventListener("load", onLoad, true); + if (gCurrentTest.pref) { + SpecialPowers.pushPrefEnv({"set": gCurrentTest.pref}, + function() { + content.location = gCurrentTest.url; + }); + } else { + content.location = gCurrentTest.url; + } +} + +function onLoad(aEvent) { + gContentBrowser.removeEventListener("load", onLoad, true); + let aOutputNode = gHud.outputNode; + + waitForSuccess({ + name: gCurrentTest.name, + validator: function() { + if (gHud.outputNode.textContent.indexOf(TRIGGER_MSG) >= 0) { + for (let warning of gCurrentTest.warning) { + if (gHud.outputNode.textContent.indexOf(warning) < 0) { + return false; + } + } + for (let nowarning of gCurrentTest.nowarning) { + if (gHud.outputNode.textContent.indexOf(nowarning) >= 0) { + return false; + } + } + return true; + } + } + }).then(runTestLoop); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js b/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js new file mode 100644 index 000000000..ab86bb49e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_change_font_size.js @@ -0,0 +1,39 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Jennifer Fong <jfong@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +const TEST_URI = "http://example.com/"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + Services.prefs.setIntPref("devtools.webconsole.fontSize", 10); + let hud = yield HUDService.toggleBrowserConsole(); + + let inputNode = hud.jsterm.inputNode; + let outputNode = hud.jsterm.outputNode; + outputNode.focus(); + + EventUtils.synthesizeKey("-", { accelKey: true }, hud.iframeWindow); + is(inputNode.style.fontSize, "10px", "input font stays at same size with ctrl+-"); + is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+-"); + + EventUtils.synthesizeKey("=", { accelKey: true }, hud.iframeWindow); + is(inputNode.style.fontSize, "11px", "input font increased with ctrl+="); + is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+="); + + EventUtils.synthesizeKey("-", { accelKey: true }, hud.iframeWindow); + is(inputNode.style.fontSize, "10px", "font decreased with ctrl+-"); + is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+-"); + + EventUtils.synthesizeKey("0", { accelKey: true }, hud.iframeWindow); + is(inputNode.style.fontSize, "", "font reset with ctrl+0"); + is(outputNode.style.fontSize, inputNode.style.fontSize, "output font stays at same size with ctrl+0"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_chrome.js new file mode 100644 index 000000000..f7f3cd0e3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js @@ -0,0 +1,38 @@ +/* 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 code completion works properly in chrome tabs, like about:credits. + +"use strict"; + +function test() { + Task.spawn(function*() { + const {tab} = yield loadTab("about:config"); + ok(tab, "tab loaded"); + + const hud = yield openConsole(tab); + ok(hud, "we have a console"); + ok(hud.iframeWindow, "we have the console UI window"); + + let jsterm = hud.jsterm; + ok(jsterm, "we have a jsterm"); + + let input = jsterm.inputNode; + ok(hud.outputNode, "we have an output node"); + + // Test typing 'docu'. + input.value = "docu"; + input.setSelectionRange(4, 4); + + let deferred = promise.defer(); + + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, function() { + is(jsterm.completeNode.value, " ment", "'docu' completion"); + deferred.resolve(null); + }); + + yield deferred.promise; + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_clickable_urls.js b/browser/devtools/webconsole/test/browser_webconsole_clickable_urls.js new file mode 100644 index 000000000..05f1ee2c9 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_clickable_urls.js @@ -0,0 +1,85 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// When strings containing URLs are entered into the webconsole, +// check its output and ensure that the output can be clicked to open those URLs. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS"; + +let inputTests = [ + + // 0: URL opens page when clicked. + { + input: "'http://example.com'", + output: "http://example.com", + expectedTab: "http://example.com/", + }, + + // 1: URL opens page using https when clicked. + { + input: "'https://example.com'", + output: "https://example.com", + expectedTab: "https://example.com/", + }, + + // 2: URL with port opens page when clicked. + { + input: "'https://example.com:443'", + output: "https://example.com:443", + expectedTab: "https://example.com/", + }, + + // 3: URL containing non-empty path opens page when clicked. + { + input: "'http://example.com/foo'", + output: "http://example.com/foo", + expectedTab: "http://example.com/foo", + }, + + // 4: URL opens page when clicked, even when surrounded by non-URL tokens. + { + input: "'foo http://example.com bar'", + output: "foo http://example.com bar", + expectedTab: "http://example.com/", + }, + + // 5: URL opens page when clicked, and whitespace is be preserved. + { + input: "'foo\\nhttp://example.com\\nbar'", + output: "foo\nhttp://example.com\nbar", + expectedTab: "http://example.com/", + }, + + // 6: URL opens page when clicked when multiple links are present. + { + input: "'http://example.com http://example.com'", + output: "http://example.com http://example.com", + expectedTab: "http://example.com/", + }, + + // 7: URL without scheme does not open page when clicked. + { + input: "'example.com'", + output: "example.com", + }, + + // 8: URL with invalid scheme does not open page when clicked. + { + input: "'foo://example.com'", + output: "foo://example.com", + }, + +]; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + inputTests = null; + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_closure_inspection.js b/browser/devtools/webconsole/test/browser_webconsole_closure_inspection.js new file mode 100644 index 000000000..537ef1a72 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_closure_inspection.js @@ -0,0 +1,89 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that inspecting a closure in the variables view sidebar works when +// execution is paused. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-closures.html"; + +let gWebConsole, gJSTerm, gVariablesView; + +function test() +{ + registerCleanupFunction(() => { + gWebConsole = gJSTerm = gVariablesView = null; + }); + + loadTab(TEST_URI).then(() => { + openConsole().then((hud) => { + openDebugger().then(({ toolbox, panelWin }) => { + let deferred = promise.defer(); + panelWin.gThreadClient.addOneTimeListener("resumed", (aEvent, aPacket) => { + ok(true, "Debugger resumed"); + deferred.resolve({ toolbox: toolbox, panelWin: panelWin }); + }); + return deferred.promise; + }).then(({ toolbox, panelWin }) => { + let deferred = promise.defer(); + panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, (aEvent, aPacket) => { + ok(true, "Scopes were fetched"); + toolbox.selectTool("webconsole").then(() => consoleOpened(hud)); + deferred.resolve(); + }); + + let button = content.document.querySelector("button"); + ok(button, "button element found"); + EventUtils.synthesizeMouseAtCenter(button, {}, content); + + return deferred.promise; + }); + }) + }); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gJSTerm.execute("window.george.getName"); + + waitForMessages({ + webconsole: gWebConsole, + messages: [{ + text: "function _pfactory/<.getName()", + category: CATEGORY_OUTPUT, + objects: true, + }], + }).then(onExecuteGetName); +} + +function onExecuteGetName(aResults) +{ + let clickable = aResults[0].clickableElements[0]; + ok(clickable, "clickable object found"); + + gJSTerm.once("variablesview-fetched", onGetNameFetch); + EventUtils.synthesizeMouse(clickable, 2, 2, {}, gWebConsole.iframeWindow); +} + +function onGetNameFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: /_pfactory/, value: "" }, + ], { webconsole: gWebConsole }).then(onExpandClosure); +} + +function onExpandClosure(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the name property in the variables view"); + + gVariablesView.window.focus(); + gJSTerm.once("sidebar-closed", finishTest); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_column_numbers.js b/browser/devtools/webconsole/test/browser_webconsole_column_numbers.js new file mode 100644 index 000000000..bbc5dde81 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_column_numbers.js @@ -0,0 +1,42 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + + // Check if console provides the right column number alongside line number + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-column.html"; + +let hud; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(aHud) { + hud = aHud; + + waitForMessages({ + webconsole: hud, + messages: [{ + text: 'Error Message', + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR + }] + }).then(testLocationColumn); +} + +function testLocationColumn() { + let messages = hud.outputNode.children; + let expected = ['10:6', '10:38', '11:8', '12:10', '13:8', '14:6']; + + for(let i = 0, len = messages.length; i < len; i++) { + let msg = messages[i].textContent; + + is(msg.contains(expected[i]), true, 'Found expected line:column of ' + expected[i]); + } + + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_completion.js b/browser/devtools/webconsole/test/browser_webconsole_completion.js new file mode 100644 index 000000000..d68ae0ac3 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js @@ -0,0 +1,101 @@ +/* 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 code completion works properly. + +const TEST_URI = "data:text/html;charset=utf8,<p>test code completion"; + +let jsterm; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + jsterm = hud.jsterm; + let input = jsterm.inputNode; + + // Test typing 'docu'. + input.value = "docu"; + input.setSelectionRange(4, 4); + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(input.value, "docu", "'docu' completion (input.value)"); + is(jsterm.completeNode.value, " ment", "'docu' completion (completeNode)"); + + // Test typing 'docu' and press tab. + input.value = "docu"; + input.setSelectionRange(4, 4); + yield complete(jsterm.COMPLETE_FORWARD); + + is(input.value, "document", "'docu' tab completion"); + is(input.selectionStart, 8, "start selection is alright"); + is(input.selectionEnd, 8, "end selection is alright"); + is(jsterm.completeNode.value.replace(/ /g, ""), "", "'docu' completed"); + + // Test typing 'window.Ob' and press tab. Just 'window.O' is + // ambiguous: could be window.Object, window.Option, etc. + input.value = "window.Ob"; + input.setSelectionRange(9, 9); + yield complete(jsterm.COMPLETE_FORWARD); + + is(input.value, "window.Object", "'window.Ob' tab completion"); + + // Test typing 'document.getElem'. + input.value = "document.getElem"; + input.setSelectionRange(16, 16); + yield complete(jsterm.COMPLETE_FORWARD); + + is(input.value, "document.getElem", "'document.getElem' completion"); + is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' completion"); + + // Test pressing tab another time. + yield jsterm.complete(jsterm.COMPLETE_FORWARD); + + is(input.value, "document.getElem", "'document.getElem' completion"); + is(jsterm.completeNode.value, " entsByTagName", "'document.getElem' another tab completion"); + + // Test pressing shift_tab. + complete(jsterm.COMPLETE_BACKWARD); + + is(input.value, "document.getElem", "'document.getElem' untab completion"); + is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' completion"); + + jsterm.clearOutput(); + + input.value = "docu"; + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(jsterm.completeNode.value, " ment", "'docu' completion"); + yield jsterm.execute(); + is(jsterm.completeNode.value, "", "clear completion on execute()"); + + // Test multi-line completion works + input.value = "console.log('one');\nconsol"; + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(jsterm.completeNode.value, " \n e", "multi-line completion"); + + // Test non-object autocompletion. + input.value = "Object.name.sl"; + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(jsterm.completeNode.value, " ice", "non-object completion"); + + // Test string literal autocompletion. + input.value = "'Asimov'.sl"; + yield complete(jsterm.COMPLETE_HINT_ONLY); + + is(jsterm.completeNode.value, " ice", "string literal completion"); + + jsterm = null; +}); + + +function complete(type) { + let updated = jsterm.once("autocomplete-updated"); + jsterm.complete(type); + return updated; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_api_stackframe.js b/browser/devtools/webconsole/test/browser_webconsole_console_api_stackframe.js new file mode 100644 index 000000000..499a45d59 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_console_api_stackframe.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the console API messages for console.error()/exception()/assert() +// include a stackframe. See bug 920116. + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-api-stackframe.html"; + const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/")); + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + + const stack = [{ + file: TEST_FILE, + fn: "thirdCall", + line: /\b2[123]\b/, // 21,22,23 + }, { + file: TEST_FILE, + fn: "secondCall", + line: 16, + }, { + file: TEST_FILE, + fn: "firstCall", + line: 12, + }]; + + let results = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo-log", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + collapsible: false, + }, { + text: "foo-error", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: stack, + }, { + text: "foo-exception", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: stack, + }, { + text: "foo-assert", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: stack, + }], + }); + + let elem = [...results[1].matched][0]; + ok(elem, "message element"); + + let msg = elem._messageObject; + ok(msg, "message object"); + + ok(msg.collapsed, "message is collapsed"); + + msg.toggleDetails(); + + ok(!msg.collapsed, "message is not collapsed"); + + msg.toggleDetails(); + + ok(msg.collapsed, "message is collapsed"); + + yield closeConsole(tab); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js b/browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js new file mode 100644 index 000000000..a522fc47b --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the '%c' modifier works with the console API. See bug 823097. + +function test() { + let hud; + + const TEST_URI = "data:text/html;charset=utf8,<p>test for " + + "console.log('%ccustom styles', 'color:red')"; + + const checks = [{ + // check the basics work + style: "color:red;font-size:1.3em", + props: { color: true, fontSize: true }, + sameStyleExpected: true, + }, { + // check that the url() is not allowed + style: "color:blue;background-image:url('http://example.com/test')", + props: { color: true, fontSize: false, background: false, + backgroundImage: false }, + sameStyleExpected: false, + }, { + // check that some properties are not allowed + style: "color:pink;position:absolute;top:10px", + props: { color: true, position: false, top: false }, + sameStyleExpected: false, + }]; + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + hud = yield openConsole(tab); + + for (let check of checks) { + yield checkStyle(check); + } + + yield closeConsole(tab); + } + + function* checkStyle(check) { + hud.jsterm.clearOutput(); + + info("checkStyle " + check.style); + hud.jsterm.execute("console.log('%cfoobar', \"" + check.style + "\")"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobar", + category: CATEGORY_WEBDEV, + }], + }); + + let msg = [...result.matched][0]; + ok(msg, "message element"); + + let span = msg.querySelector(".message-body span[style]"); + ok(span, "span element"); + + info("span textContent is: " + span.textContent); + isnot(span.textContent.indexOf("foobar"), -1, "span textContent check"); + + let outputStyle = span.getAttribute("style").replace(/\s+|;+$/g, ""); + if (check.sameStyleExpected) { + is(outputStyle, check.style, "span style is correct"); + } else { + isnot(outputStyle, check.style, "span style is not the same"); + } + + for (let prop of Object.keys(check.props)) { + is(!!span.style[prop], check.props[prop], "property check for " + prop); + } + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_extras.js b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js new file mode 100644 index 000000000..b42437492 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js @@ -0,0 +1,40 @@ +/* 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/. */ + +// Test that window.console functions that are not implemented yet do not +// output anything in the web console and they do not throw any exceptions. +// See bug 614350. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-extras.html"; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(hud) { + waitForMessages({ + webconsole: hud, + messages: [{ + text: "start", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "end", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(() => { + let nodes = hud.outputNode.querySelectorAll(".message"); + is(nodes.length, 2, "only two messages are displayed"); + finishTest(); + }); + + let button = content.document.querySelector("button"); + ok(button, "we have the button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js new file mode 100644 index 000000000..aeb73c58e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js @@ -0,0 +1,101 @@ +/* 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 the basic console.log()-style APIs and filtering work. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let outputNode = hud.outputNode; + + let methods = ["log", "info", "warn", "error", "exception", "debug"]; + for (let method of methods) { + yield testMethod(method, hud, outputNode); + } +}); + +function* testMethod(aMethod, aHud, aOutputNode) { + let console = content.console; + + aHud.jsterm.clearOutput(); + + console[aMethod]("foo-bar-baz"); + console[aMethod]("baar-baz"); + + yield waitForMessages({ + webconsole: aHud, + messages: [{ + text: "foo-bar-baz", + }, { + text: "baar-baz", + }], + }); + + setStringFilter("foo", aHud); + + is(aOutputNode.querySelectorAll(".filtered-by-string").length, 1, + "1 hidden " + aMethod + " node via string filtering") + + aHud.jsterm.clearOutput(); + + // now toggle the current method off - make sure no visible message + // TODO: move all filtering tests into a separate test file: see bug 608135 + + console[aMethod]("foo-bar-baz"); + yield waitForMessages({ + webconsole: aHud, + messages: [{ + text: "foo-bar-baz", + }], + }); + + setStringFilter("", aHud); + let filter; + switch(aMethod) { + case "debug": + filter = "log"; + break; + case "exception": + filter = "error"; + break; + default: + filter = aMethod; + break; + } + + aHud.setFilterState(filter, false); + + is(aOutputNode.querySelectorAll(".filtered-by-type").length, 1, + "1 message hidden for " + aMethod + " (logging turned off)") + + aHud.setFilterState(filter, true); + + is(aOutputNode.querySelectorAll(".message:not(.filtered-by-type)").length, 1, + "1 message shown for " + aMethod + " (logging turned on)") + + aHud.jsterm.clearOutput(); + + // test for multiple arguments. + console[aMethod]("foo", "bar"); + + yield waitForMessages({ + webconsole: aHud, + messages: [{ + text: '"foo" "bar"', + category: CATEGORY_WEBDEV, + }], + }) +} + +function setStringFilter(aValue, aHud) { + aHud.ui.filterBox.value = aValue; + aHud.ui.adjustVisibilityOnSearchStringChange(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_console_trace_duplicates.js b/browser/devtools/webconsole/test/browser_webconsole_console_trace_duplicates.js new file mode 100644 index 000000000..76793c3c7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_console_trace_duplicates.js @@ -0,0 +1,46 @@ +/* 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/. */ + +function test() { + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html"; + + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + const hud = yield openConsole(tab); + + content.location = TEST_URI; + + // NB: Now that stack frames include a column number multiple invocations + // on the same line are considered unique. ie: + // |foo(); foo();| + // will generate two distinct trace entries. + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.trace output for foo1()", + text: "foo1()", + consoleTrace: { + file: "test-bug_939783_console_trace_duplicates.html", + fn: "foo3()", + }, + }, { + name: "console.trace output for foo1()", + text: "foo1()", + consoleTrace: { + file: "test-bug_939783_console_trace_duplicates.html", + fn: "foo3()", + }, + }, { + name: "console.trace output for foo1b()", + text: "foo1b()", + consoleTrace: { + file: "test-bug_939783_console_trace_duplicates.html", + fn: "foo3()", + }, + }], + }); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_count.js b/browser/devtools/webconsole/test/browser_webconsole_count.js new file mode 100644 index 000000000..a4866baa4 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_count.js @@ -0,0 +1,77 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that console.count() counts as expected. See bug 922208. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-count.html"; + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + + let button = content.document.querySelector("#local"); + ok(button, "we have the local-tests button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + let messages = []; + [ + "start", + "<no label>: 2", + "console.count() testcounter: 1", + "console.count() testcounter: 2", + "console.count() testcounter: 3", + "console.count() testcounter: 4", + "end" + ].forEach(function (msg) { + messages.push({ + text: msg, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }); + }); + messages.push({ + name: "Three local counts with no label and count=1", + text: "<no label>: 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 3 + }); + yield waitForMessages({ + webconsole: hud, + messages: messages + }); + + hud.jsterm.clearOutput(); + + button = content.document.querySelector("#external"); + ok(button, "we have the external-tests button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + messages = []; + [ + "start", + "console.count() testcounter: 5", + "console.count() testcounter: 6", + "end" + ].forEach(function (msg) { + messages.push({ + text: msg, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }); + }); + messages.push({ + name: "Two external counts with no label and count=1", + text: "<no label>: 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2 + }); + yield waitForMessages({ + webconsole: hud, + messages: messages + }); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js b/browser/devtools/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js new file mode 100644 index 000000000..34eb87b00 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js @@ -0,0 +1,50 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that if a link in console is double clicked, the console frame doesn't +// navigate to that destination (bug 975707). + +"use strict"; + +function test() { + let originalNetPref = Services.prefs.getBoolPref("devtools.webconsole.filter.networkinfo"); + registerCleanupFunction(() => { + Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", originalNetPref); + }); + Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true); + Task.spawn(runner).then(finishTest); + + function* runner() { + const TEST_PAGE_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html" + "?_uniq=" + Date.now(); + + const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>"); + const hud = yield openConsole(tab); + + content.location = TEST_PAGE_URI; + + let messages = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "Network request message", + url: TEST_PAGE_URI, + category: CATEGORY_NETWORK + }] + }); + + let networkEventMessage = messages[0].matched.values().next().value; + let urlNode = networkEventMessage.querySelector(".url"); + + let deferred = promise.defer(); + urlNode.addEventListener("click", function onClick(aEvent) { + urlNode.removeEventListener("click", onClick); + ok(aEvent.defaultPrevented, "The default action was prevented."); + + deferred.resolve(); + }); + + EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2}, hud.iframeWindow); + + yield deferred.promise; + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js new file mode 100644 index 000000000..338b17e16 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js @@ -0,0 +1,34 @@ +/* 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 commands run by the user are executed in content space. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + hud.jsterm.execute("window.location.href;"); + + let [input, output] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "window.location.href;", + category: CATEGORY_INPUT, + }, + { + text: TEST_URI, + category: CATEGORY_OUTPUT, + }], + }); + + let inputNode = [...input.matched][0]; + let outputNode = [...output.matched][0]; + is(inputNode.getAttribute("category"), "input", "input node category is correct"); + is(outputNode.getAttribute("category"), "output", "output node category is correct"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_expandable_timestamps.js b/browser/devtools/webconsole/test/browser_webconsole_expandable_timestamps.js new file mode 100644 index 000000000..efcdaa63c --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_expandable_timestamps.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for the message timestamps option: check if the preference toggles the +// display of messages in the console output. See bug 722267. + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 722267 - " + + "preference for toggling timestamps in messages"; +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; +let hud; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + let panel = yield consoleOpened(); + + yield onOptionsPanelSelected(panel); + onPrefChanged(); + + Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP); + hud = null; +}); + +function consoleOpened() +{ + info("console opened"); + let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP); + ok(!prefValue, "messages have no timestamp by default (pref check)"); + ok(hud.outputNode.classList.contains("hideTimestamps"), + "messages have no timestamp (class name check)"); + + let toolbox = gDevTools.getToolbox(hud.target); + return toolbox.selectTool("options"); +} + +function onOptionsPanelSelected(panel) +{ + info("options panel opened"); + + let prefChanged = gDevTools.once("pref-changed", onPrefChanged); + + let checkbox = panel.panelDoc.getElementById("webconsole-timestamp-messages"); + checkbox.click(); + + return prefChanged; +} + +function onPrefChanged() +{ + info("pref changed"); + let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP); + ok(prefValue, "messages have timestamps (pref check)"); + ok(!hud.outputNode.classList.contains("hideTimestamps"), + "messages have timestamps (class name check)"); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js b/browser/devtools/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js new file mode 100644 index 000000000..058b52afa --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the filter button context menu logic works correctly. + +const TEST_URI = "http://example.com/"; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(testFilterButtons); + }) +} + +function testFilterButtons(aHud) { + let hudBox = aHud.ui.rootElement; + + testRightClick("net", hudBox, aHud) + .then(() => testRightClick("css", hudBox, aHud)) + .then(() => testRightClick("js", hudBox, aHud)) + .then(() => testRightClick("logging", hudBox, aHud)) + .then(() => testRightClick("security", hudBox, aHud)) + .then(finishTest); +} + +function testRightClick(aCategory, hudBox, aHud) { + let deferred = promise.defer(); + let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]"; + let button = hudBox.querySelector(selector); + let mainButton = getMainButton(button, aHud); + let origCheckedState = button.getAttribute("aria-pressed"); + let contextMenu = aHud.iframeWindow.document.getElementById(aCategory + "-contextmenu"); + + function verifyContextMenuIsClosed() { + info("verify the context menu is closed"); + is(button.getAttribute("open"), false, "The context menu for the \"" + aCategory + + "\" button is closed"); + } + + function verifyOriginalCheckedState() { + info("verify the button has the original checked state"); + is(button.getAttribute("aria-pressed"), origCheckedState, + "The button state should not have changed"); + }; + + function verifyNewCheckedState() { + info("verify the button's checked state has changed"); + isnot(button.getAttribute("aria-pressed"), origCheckedState, + "The button state should have changed"); + }; + + function leftClickToClose() { + info("left click the button to close the contextMenu"); + EventUtils.sendMouseEvent({type: "click"}, button); + executeSoon(() => { + verifyContextMenuIsClosed(); + verifyOriginalCheckedState(); + leftClickToChangeCheckedState(); + }); + } + + function leftClickToChangeCheckedState() { + info("left click the mainbutton to change checked state"); + EventUtils.sendMouseEvent({type: "click"}, mainButton); + executeSoon(() => { + verifyContextMenuIsClosed(); + verifyNewCheckedState(); + deferred.resolve(); + }); + } + + verifyContextMenuIsClosed(); + info("right click the button to open the context menu"); + waitForContextMenu(contextMenu, mainButton, verifyOriginalCheckedState, + leftClickToClose); + return deferred.promise; +} + +function getMainButton(aTargetButton, aHud) { + let anonymousNodes = aHud.ui.document.getAnonymousNodes(aTargetButton); + let subbutton; + + for (let i = 0; i < anonymousNodes.length; i++) { + let node = anonymousNodes[i]; + if (node.classList.contains("toolbarbutton-menubutton-button")) { + subbutton = node; + break; + } + } + + return subbutton; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_for_of.js b/browser/devtools/webconsole/test/browser_webconsole_for_of.js new file mode 100644 index 000000000..b5b6304f6 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_for_of.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// A for-of loop in Web Console code can loop over a content NodeList. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-for-of.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testForOf(hud); +}); + +function testForOf(hud) { + let deferred = promise.defer(); + + var jsterm = hud.jsterm; + jsterm.execute("{ [x.tagName for (x of document.body.childNodes) if (x.nodeType === 1)].join(' '); }", + (node) => { + ok(/H1 DIV H2 P/.test(node.textContent), + "for-of loop should find all top-level nodes"); + deferred.resolve(); + }); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_history.js b/browser/devtools/webconsole/test/browser_webconsole_history.js new file mode 100644 index 000000000..42d957afb --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_history.js @@ -0,0 +1,61 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests the console history feature accessed via the up and down arrow keys. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +// Constants used for defining the direction of JSTerm input history navigation. +const HISTORY_BACK = -1; +const HISTORY_FORWARD = 1; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let jsterm = hud.jsterm; + let input = jsterm.inputNode; + + let executeList = ["document", "window", "window.location"]; + + for each (var item in executeList) { + input.value = item; + yield jsterm.execute(); + } + + for (var i = executeList.length - 1; i != -1; i--) { + jsterm.historyPeruse(HISTORY_BACK); + is (input.value, executeList[i], "check history previous idx:" + i); + } + + jsterm.historyPeruse(HISTORY_BACK); + is (input.value, executeList[0], "test that item is still index 0"); + + jsterm.historyPeruse(HISTORY_BACK); + is (input.value, executeList[0], "test that item is still still index 0"); + + for (var i = 1; i < executeList.length; i++) { + jsterm.historyPeruse(HISTORY_FORWARD); + is (input.value, executeList[i], "check history next idx:" + i); + } + + jsterm.historyPeruse(HISTORY_FORWARD); + is (input.value, "", "check input is empty again"); + + // Simulate pressing Arrow_Down a few times and then if Arrow_Up shows + // the previous item from history again. + jsterm.historyPeruse(HISTORY_FORWARD); + jsterm.historyPeruse(HISTORY_FORWARD); + jsterm.historyPeruse(HISTORY_FORWARD); + + is (input.value, "", "check input is still empty"); + + let idxLast = executeList.length - 1; + jsterm.historyPeruse(HISTORY_BACK); + is (input.value, executeList[idxLast], "check history next idx:" + idxLast); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js b/browser/devtools/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js new file mode 100644 index 000000000..192fc5595 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that the JS input field is focused when the user switches back to the +// web console from other tools, see bug 891581. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>hello"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + is(hud.jsterm.inputNode.hasAttribute("focused"), true, + "inputNode should be focused"); + + hud.ui.filterBox.focus(); + + is(hud.ui.filterBox.hasAttribute("focused"), true, + "filterBox should be focused"); + + is(hud.jsterm.inputNode.hasAttribute("focused"), false, + "inputNode shouldn't be focused"); + + yield openDebugger(); + hud = yield openConsole(); + + is(hud.jsterm.inputNode.hasAttribute("focused"), true, + "inputNode should be focused"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_inspect-parsed-documents.js b/browser/devtools/webconsole/test/browser_webconsole_inspect-parsed-documents.js new file mode 100644 index 000000000..e55e021c2 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_inspect-parsed-documents.js @@ -0,0 +1,34 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that dynamically created (HTML|XML|SVG)Documents can be inspected by +// clicking on the object in console (bug 1035198). + +"use strict;" + +const TEST_CASES = [ + { + input: '(new DOMParser()).parseFromString("<a />", "text/html")', + output: "HTMLDocument", + inspectable: true, + }, + { + input: '(new DOMParser()).parseFromString("<a />", "application/xml")', + output: "XMLDocument", + inspectable: true, + }, + { + input: '(new DOMParser()).parseFromString("<svg></svg>", "image/svg+xml")', + output: "SVGDocument", + inspectable: true, + }, +]; + +const TEST_URI = "data:text/html;charset=utf8," + + "browser_webconsole_inspect-parsed-documents.js"; +add_task(function* () { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkOutputForInputs(hud, TEST_CASES); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js b/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js new file mode 100644 index 000000000..fe0483233 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_js_input_expansion.js @@ -0,0 +1,55 @@ +/* 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 the input box expands as the user types long lines. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let input = hud.jsterm.inputNode; + input.focus(); + + is(input.getAttribute("multiline"), "true", "multiline is enabled"); + // Tests if the inputNode expands. + input.value = "hello\nworld\n"; + let length = input.value.length; + input.selectionEnd = length; + input.selectionStart = length; + function getHeight() + { + return input.clientHeight; + } + let initialHeight = getHeight(); + // Performs an "d". This will trigger/test for the input event that should + // change the "row" attribute of the inputNode. + EventUtils.synthesizeKey("d", {}); + let newHeight = getHeight(); + ok(initialHeight < newHeight, "Height changed: " + newHeight); + + // Add some more rows. Tests for the 8 row limit. + input.value = "row1\nrow2\nrow3\nrow4\nrow5\nrow6\nrow7\nrow8\nrow9\nrow10\n"; + length = input.value.length; + input.selectionEnd = length; + input.selectionStart = length; + EventUtils.synthesizeKey("d", {}); + let newerHeight = getHeight(); + + ok(newerHeight > newHeight, "height changed: " + newerHeight); + + // Test if the inputNode shrinks again. + input.value = ""; + EventUtils.synthesizeKey("d", {}); + let height = getHeight(); + info("height: " + height); + info("initialHeight: " + initialHeight); + let finalHeightDifference = Math.abs(initialHeight - height); + ok(finalHeightDifference <= 1, "height shrank to original size within 1px"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js new file mode 100644 index 000000000..8b9100d0e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js @@ -0,0 +1,144 @@ +/* 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/. */ + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let jsterm; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + jsterm = hud.jsterm; + yield testJSTerm(hud); + jsterm = null; +}); + +function checkResult(msg, desc) { + let def = promise.defer(); + waitForMessages({ + webconsole: jsterm.hud.owner, + messages: [{ + name: desc, + category: CATEGORY_OUTPUT, + }], + }).then(([result]) => { + let node = [...result.matched][0].querySelector(".message-body"); + if (typeof msg == "string") { + is(node.textContent.trim(), msg, + "correct message shown for " + desc); + } + else if (typeof msg == "function") { + ok(msg(node), "correct message shown for " + desc); + } + + def.resolve(); + }); + return def.promise; +} + +function testJSTerm(hud) +{ + const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + + jsterm.clearOutput(); + yield jsterm.execute("$('#header').getAttribute('id')"); + yield checkResult('"header"', "$() worked"); + + jsterm.clearOutput(); + yield jsterm.execute("$$('h1').length"); + yield checkResult("1", "$$() worked"); + + jsterm.clearOutput(); + yield jsterm.execute("$x('.//*', document.body)[0] == $$('h1')[0]"); + yield checkResult("true", "$x() worked"); + + // no jsterm.clearOutput() here as we clear the output using the clear() fn. + yield jsterm.execute("clear()"); + + yield waitForSuccess({ + name: "clear() worked", + validator: function() + { + return jsterm.outputNode.childNodes.length == 0; + } + }); + + jsterm.clearOutput(); + yield jsterm.execute("keys({b:1})[0] == 'b'"); + yield checkResult("true", "keys() worked", 1); + + jsterm.clearOutput(); + yield jsterm.execute("values({b:1})[0] == 1"); + yield checkResult("true", "values() worked", 1); + + jsterm.clearOutput(); + + let openedLinks = 0; + let oldOpenLink = hud.openLink; + hud.openLink = (url) => { + if (url == HELP_URL) { + openedLinks++; + } + }; + + yield jsterm.execute("help()"); + yield jsterm.execute("help"); + yield jsterm.execute("?"); + + let output = jsterm.outputNode.querySelector(".message[category='output']"); + ok(!output, "no output for help() calls"); + is(openedLinks, 3, "correct number of pages opened by the help calls"); + hud.openLink = oldOpenLink; + + jsterm.clearOutput(); + yield jsterm.execute("pprint({b:2, a:1})"); + yield checkResult("\" b: 2\n a: 1\"", "pprint()"); + + // check instanceof correctness, bug 599940 + jsterm.clearOutput(); + yield jsterm.execute("[] instanceof Array"); + yield checkResult("true", "[] instanceof Array == true"); + + jsterm.clearOutput(); + yield jsterm.execute("({}) instanceof Object"); + yield checkResult("true", "({}) instanceof Object == true"); + + // check for occurrences of Object XRayWrapper, bug 604430 + jsterm.clearOutput(); + yield jsterm.execute("document"); + yield checkResult(function(node) { + return node.textContent.search(/\[object xraywrapper/i) == -1; + }, "document - no XrayWrapper"); + + // check that pprint(window) and keys(window) don't throw, bug 608358 + jsterm.clearOutput(); + yield jsterm.execute("pprint(window)"); + yield checkResult(null, "pprint(window)"); + + jsterm.clearOutput(); + yield jsterm.execute("keys(window)"); + yield checkResult(null, "keys(window)"); + + // bug 614561 + jsterm.clearOutput(); + yield jsterm.execute("pprint('hi')"); + yield checkResult("\" 0: \"h\"\n 1: \"i\"\"", "pprint('hi')"); + + // check that pprint(function) shows function source, bug 618344 + jsterm.clearOutput(); + yield jsterm.execute("pprint(function() { var someCanaryValue = 42; })"); + yield checkResult(function(node) { + return node.textContent.indexOf("someCanaryValue") > -1; + }, "pprint(function) shows source"); + + // check that an evaluated null produces "null", bug 650780 + jsterm.clearOutput(); + yield jsterm.execute("null"); + yield checkResult("null", "null is null"); + + jsterm.clearOutput(); + yield jsterm.execute("undefined"); + yield checkResult("undefined", "undefined is printed"); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js new file mode 100644 index 000000000..02b79b6e9 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js @@ -0,0 +1,55 @@ +/* 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 the message type filter checkboxes work. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let console = content.console; + + for (let i = 0; i < 50; i++) { + console.log("foobarz #" + i); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz #49", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + is(hud.outputNode.children.length, 50, "number of messages"); + + hud.setFilterState("log", false); + is(countMessageNodes(hud), 0, "the log nodes are hidden when the " + + "corresponding filter is switched off"); + + hud.setFilterState("log", true); + is(countMessageNodes(hud), 50, "the log nodes reappear when the " + + "corresponding filter is switched on"); +}); + +function countMessageNodes(hud) { + let messageNodes = hud.outputNode.querySelectorAll(".message"); + let displayedMessageNodes = 0; + let view = hud.iframeWindow; + for (let i = 0; i < messageNodes.length; i++) { + let computedStyle = view.getComputedStyle(messageNodes[i], null); + if (computedStyle.display !== "none") { + displayedMessageNodes++; + } + } + + return displayedMessageNodes; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js new file mode 100644 index 000000000..fb229075a --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js @@ -0,0 +1,96 @@ +/* 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 the text filter box works. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let console = content.console; + + for (let i = 0; i < 50; i++) { + console.log("http://www.example.com/ " + i); + } + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "http://www.example.com/ 49", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }) + + is(hud.outputNode.children.length, 50, "number of messages"); + + setStringFilter(hud, "http"); + isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " + + "search string is set to \"http\""); + + setStringFilter(hud, "hxxp"); + is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " + + "string is set to \"hxxp\""); + + setStringFilter(hud, "ht tp"); + isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " + + "search string is set to \"ht tp\""); + + setStringFilter(hud, " zzzz zzzz "); + is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " + + "string is set to \" zzzz zzzz \""); + + setStringFilter(hud, ""); + isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " + + "search string is removed"); + + setStringFilter(hud, "\u9f2c"); + is(countMessageNodes(hud), 0, "the log nodes are hidden when searching " + + "for weasels"); + + setStringFilter(hud, "\u0007"); + is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " + + "the bell character"); + + setStringFilter(hud, '"foo"'); + is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " + + 'the string "foo"'); + + setStringFilter(hud, "'foo'"); + is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " + + "the string 'foo'"); + + setStringFilter(hud, "foo\"bar'baz\"boo'"); + is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " + + "the string \"foo\"bar'baz\"boo'\""); +}); + +function countMessageNodes(hud) { + let outputNode = hud.outputNode; + + let messageNodes = outputNode.querySelectorAll(".message"); + let displayedMessageNodes = 0; + let view = hud.iframeWindow; + for (let i = 0; i < messageNodes.length; i++) { + let computedStyle = view.getComputedStyle(messageNodes[i], null); + if (computedStyle.display !== "none") { + displayedMessageNodes++; + } + } + + return displayedMessageNodes; +} + +function setStringFilter(hud, aValue) +{ + hud.ui.filterBox.value = aValue; + hud.ui.adjustVisibilityOnSearchStringChange(); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_log_file_filter.js b/browser/devtools/webconsole/test/browser_webconsole_log_file_filter.js new file mode 100644 index 000000000..906bb2b48 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_log_file_filter.js @@ -0,0 +1,82 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the text filter box works to filter based on filenames +// where the logs were generated. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug_923281_console_log_filter.html"; + +let hud; + +"use strict"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + yield consoleOpened(); + + testLiveFilteringOnSearchStrings(); + + hud = null; +}); + +function consoleOpened() { + let console = content.console; + console.log("sentinel log"); + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "sentinel log", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }], + }) +} + +function testLiveFilteringOnSearchStrings() { + is(hud.outputNode.children.length, 4, "number of messages"); + + setStringFilter("random"); + is(countMessageNodes(), 1, "the log nodes not containing string " + + "\"random\" are hidden"); + + setStringFilter("test2.js"); + is(countMessageNodes(), 2, "show only log nodes containing string " + + "\"test2.js\" or log nodes created from files with filename " + + "containing \"test2.js\" as substring."); + + setStringFilter("test1"); + is(countMessageNodes(), 2, "show only log nodes containing string " + + "\"test1\" or log nodes created from files with filename " + + "containing \"test1\" as substring."); + + setStringFilter(""); + is(countMessageNodes(), 4, "show all log nodes on setting filter string " + + "as \"\"."); +} + +function countMessageNodes() { + let outputNode = hud.outputNode; + + let messageNodes = outputNode.querySelectorAll(".message"); + content.console.log(messageNodes.length); + let displayedMessageNodes = 0; + let view = hud.iframeWindow; + for (let i = 0; i < messageNodes.length; i++) { + let computedStyle = view.getComputedStyle(messageNodes[i], null); + if (computedStyle.display !== "none") { + displayedMessageNodes++; + } + } + + return displayedMessageNodes; +} + +function setStringFilter(aValue) +{ + hud.ui.filterBox.value = aValue; + hud.ui.adjustVisibilityOnSearchStringChange(); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js new file mode 100644 index 000000000..0a94538e6 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js @@ -0,0 +1,27 @@ +/* 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/. */ + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + hud.jsterm.execute("console.log('a log message')"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "a log message", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let msg = [...result.matched][0]; + ok(msg.getAttribute("id"), "log message has an ID"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js new file mode 100644 index 000000000..7cf95822e --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js @@ -0,0 +1,213 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Julian Viereck <jviereck@mozilla.com> + * Patrick Walton <pcwalton@mozilla.com> + * Mihai Șucan <mihai.sucan@gmail.com> + * + * ***** END LICENSE BLOCK ***** */ + +// Tests that network log messages bring up the network panel. + +const TEST_URI = "data:text/html;charset=utf-8,Web Console network logging tests"; + +const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html"; + +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; + +const TEST_DATA_JSON_CONTENT = + '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; + +let lastRequest = null; +let requestCallback = null; +let browser, hud; + +function test() +{ + loadTab(TEST_URI).then((tab) => { + browser = tab.browser; + + openConsole().then((aHud) => { + hud = aHud; + + HUDService.lastFinishedRequest.callback = requestCallbackWrapper; + + executeSoon(testPageLoad); + }); + }); +} + +function requestCallbackWrapper(aRequest) +{ + lastRequest = aRequest; + + hud.ui.webConsoleClient.getResponseContent(lastRequest.actor, + function(aResponse) { + lastRequest.response.content = aResponse.content; + lastRequest.discardResponseBody = aResponse.contentDiscarded; + + hud.ui.webConsoleClient.getRequestPostData(lastRequest.actor, + function(aResponse) { + lastRequest.request.postData = aResponse.postData; + lastRequest.discardRequestBody = aResponse.postDataDiscarded; + + if (requestCallback) { + requestCallback(); + } + }); + }); +} + +function testPageLoad() +{ + requestCallback = function() { + // Check if page load was logged correctly. + ok(lastRequest, "Page load was logged"); + + is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI, + "Logged network entry is page load"); + is(lastRequest.request.method, "GET", "Method is correct"); + ok(!lastRequest.request.postData.text, "No request body was stored"); + ok(lastRequest.discardRequestBody, "Request body was discarded"); + ok(!lastRequest.response.content.text, "No response body was stored"); + ok(lastRequest.discardResponseBody, "Response body was discarded"); + + lastRequest = null; + requestCallback = null; + executeSoon(testPageLoadBody); + }; + + content.location = TEST_NETWORK_REQUEST_URI; +} + +function testPageLoadBody() +{ + // Turn on logging of request bodies and check again. + hud.ui.setSaveRequestAndResponseBodies(true).then(() => { + ok(hud.ui._saveRequestAndResponseBodies, + "The saveRequestAndResponseBodies property was successfully set."); + + testPageLoadBodyAfterSettingUpdate(); + }); +} + +function testPageLoadBodyAfterSettingUpdate() +{ + let loaded = false; + let requestCallbackInvoked = false; + + requestCallback = function() { + ok(lastRequest, "Page load was logged again"); + ok(!lastRequest.discardResponseBody, "Response body was not discarded"); + is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0, + "Response body's beginning is okay"); + + lastRequest = null; + requestCallback = null; + requestCallbackInvoked = true; + + if (loaded) { + executeSoon(testXhrGet); + } + }; + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + loaded = true; + + if (requestCallbackInvoked) { + executeSoon(testXhrGet); + } + }, true); + + content.location.reload(); +} + +function testXhrGet() +{ + requestCallback = function() { + ok(lastRequest, "testXhrGet() was logged"); + is(lastRequest.request.method, "GET", "Method is correct"); + ok(!lastRequest.request.postData.text, "No request body was sent"); + ok(!lastRequest.discardRequestBody, "Request body was not discarded"); + is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT, + "Response is correct"); + + lastRequest = null; + requestCallback = null; + executeSoon(testXhrPost); + }; + + // Start the XMLHttpRequest() GET test. + content.wrappedJSObject.testXhrGet(); +} + +function testXhrPost() +{ + requestCallback = function() { + ok(lastRequest, "testXhrPost() was logged"); + is(lastRequest.request.method, "POST", "Method is correct"); + is(lastRequest.request.postData.text, "Hello world!", + "Request body was logged"); + is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT, + "Response is correct"); + + lastRequest = null; + requestCallback = null; + executeSoon(testFormSubmission); + }; + + // Start the XMLHttpRequest() POST test. + content.wrappedJSObject.testXhrPost(); +} + +function testFormSubmission() +{ + // Start the form submission test. As the form is submitted, the page is + // loaded again. Bind to the load event to catch when this is done. + requestCallback = function() { + ok(lastRequest, "testFormSubmission() was logged"); + is(lastRequest.request.method, "POST", "Method is correct"); + isnot(lastRequest.request.postData.text. + indexOf("Content-Type: application/x-www-form-urlencoded"), -1, + "Content-Type is correct"); + isnot(lastRequest.request.postData.text. + indexOf("Content-Length: 20"), -1, "Content-length is correct"); + isnot(lastRequest.request.postData.text. + indexOf("name=foo+bar&age=144"), -1, "Form data is correct"); + is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0, + "Response body's beginning is okay"); + + executeSoon(testNetworkPanel); + }; + + let form = content.document.querySelector("form"); + ok(form, "we have the HTML form"); + form.submit(); +} + +function testNetworkPanel() +{ + // Open the NetworkPanel. The functionality of the NetworkPanel is tested + // within separate test files. + let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest); + + networkPanel.panel.addEventListener("popupshown", function onPopupShown() { + networkPanel.panel.removeEventListener("popupshown", onPopupShown, true); + + is(hud.ui.filterBox._netPanel, networkPanel, + "Network panel stored on anchor node"); + ok(true, "NetworkPanel was opened"); + + // All tests are done. Shutdown. + networkPanel.panel.hidePopup(); + lastRequest = null; + HUDService.lastFinishedRequest.callback = null; + browser = requestCallback = hud = null; + executeSoon(finishTest); + }, true); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js new file mode 100644 index 000000000..0cea84fea --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js @@ -0,0 +1,541 @@ +/* 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 the network panel works. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png"; +const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html"; + +const TEST_IMG_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJREFU" + + "OI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuFDjq5dxD8" + + "FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNUyhsKAChRBQcP" + + "FVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQEod+Z5sP1FizDxGgy" + + "BqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTsDizDRMPuIOC+zEeTMZo9" + + "BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joREN2uqnlBmCwW1hIJagtev4f3z" + + "A16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOnidadmwtwsnMu18q83/kHSou+bFND" + + "Dr4AAAAASUVORK5CYII="; + +let testDriver, hud; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then(testNetworkPanel); + }); +} + +function testNetworkPanel(aHud) { + hud = aHud; + testDriver = testGen(); + testDriver.next(); +} + +function checkIsVisible(aPanel, aList) { + for (let id in aList) { + let node = aPanel.document.getElementById(id); + let isVisible = aList[id]; + is(node.style.display, (isVisible ? "block" : "none"), id + " isVisible=" + isVisible); + } +} + +function checkNodeContent(aPanel, aId, aContent) { + let node = aPanel.document.getElementById(aId); + if (node == null) { + ok(false, "Tried to access node " + aId + " that doesn't exist!"); + } + else if (node.textContent.indexOf(aContent) != -1) { + ok(true, "checking content of " + aId); + } + else { + ok(false, "Got false value for " + aId + ": " + node.textContent + " doesn't have " + aContent); + } +} + +function checkNodeKeyValue(aPanel, aId, aKey, aValue) { + let node = aPanel.document.getElementById(aId); + + let headers = node.querySelectorAll("th"); + for (let i = 0; i < headers.length; i++) { + if (headers[i].textContent == (aKey + ":")) { + is(headers[i].nextElementSibling.textContent, aValue, + "checking content of " + aId + " for key " + aKey); + return; + } + } + + ok(false, "content check failed for " + aId + ", key " + aKey); +} + +function testGen() { + let filterBox = hud.ui.filterBox; + + let httpActivity = { + updates: [], + discardRequestBody: true, + discardResponseBody: true, + startedDateTime: (new Date()).toISOString(), + request: { + url: "http://www.testpage.com", + method: "GET", + cookies: [], + headers: [ + { name: "foo", value: "bar" }, + ], + }, + response: { + headers: [], + content: {}, + cookies: [], + }, + timings: {}, + }; + + let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + + is(filterBox._netPanel, networkPanel, + "Network panel stored on the anchor object"); + + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + info("test 1"); + + checkIsVisible(networkPanel, { + requestCookie: false, + requestFormData: false, + requestBody: false, + responseContainer: false, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeContent(networkPanel, "header", "http://www.testpage.com"); + checkNodeContent(networkPanel, "header", "GET"); + checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar"); + + // Test request body. + info("test 2: request body"); + httpActivity.discardRequestBody = false; + httpActivity.request.postData = { text: "hello world" }; + networkPanel.update(); + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: false, + responseContainer: false, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + checkNodeContent(networkPanel, "requestBodyContent", "hello world"); + + // Test response header. + info("test 3: response header"); + httpActivity.timings.wait = 10; + httpActivity.response.httpVersion = "HTTP/3.14"; + httpActivity.response.status = 999; + httpActivity.response.statusText = "earthquake win"; + httpActivity.response.content.mimeType = "text/html"; + httpActivity.response.headers.push( + { name: "Content-Type", value: "text/html" }, + { name: "leaveHouses", value: "true" } + ); + + networkPanel.update(); + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: false, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeContent(networkPanel, "header", "HTTP/3.14 999 earthquake win"); + checkNodeKeyValue(networkPanel, "responseHeadersContent", "leaveHouses", "true"); + checkNodeContent(networkPanel, "responseHeadersInfo", "10ms"); + + info("test 4"); + + httpActivity.discardResponseBody = false; + httpActivity.timings.receive = 2; + networkPanel.update(); + + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + requestFormData: false, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + info("test 5"); + + httpActivity.updates.push("responseContent", "eventTimings"); + networkPanel.update(); + + checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms"); + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + responseContainer: true, + responseBody: false, + responseNoBody: true, + responseImage: false, + responseImageCached: false + }); + + networkPanel.panel.hidePopup(); + + // Second run: Test for cookies and response body. + info("test 6: cookies and response body"); + httpActivity.request.cookies.push( + { name: "foo", value: "bar" }, + { name: "hello", value: "world" } + ); + httpActivity.response.content.text = "get out here"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + is(filterBox._netPanel, networkPanel, + "Network panel stored on httpActivity object"); + + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: true, + responseContainer: true, + responseCookie: false, + responseBody: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeKeyValue(networkPanel, "requestCookieContent", "foo", "bar"); + checkNodeKeyValue(networkPanel, "requestCookieContent", "hello", "world"); + checkNodeContent(networkPanel, "responseBodyContent", "get out here"); + checkNodeContent(networkPanel, "responseBodyInfo", "2ms"); + + networkPanel.panel.hidePopup(); + + // Third run: Test for response cookies. + info("test 6b: response cookies"); + httpActivity.response.cookies.push( + { name: "foobar", value: "boom" }, + { name: "foobaz", value: "omg" } + ); + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + is(filterBox._netPanel, networkPanel, + "Network panel stored on httpActivity object"); + + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: true, + responseContainer: true, + responseCookie: true, + responseBody: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false, + responseBodyFetchLink: false, + }); + + checkNodeKeyValue(networkPanel, "responseCookieContent", "foobar", "boom"); + checkNodeKeyValue(networkPanel, "responseCookieContent", "foobaz", "omg"); + + networkPanel.panel.hidePopup(); + + // Check image request. + info("test 7: image request"); + httpActivity.response.headers[1].value = "image/png"; + httpActivity.response.content.mimeType = "image/png"; + httpActivity.response.content.text = TEST_IMG_BASE64; + httpActivity.request.url = TEST_IMG; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: true, + responseImageCached: false, + responseBodyFetchLink: false, + }); + + let imgNode = networkPanel.document.getElementById("responseImageNode"); + is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64, + "Displayed image is correct"); + + function checkImageResponseInfo() { + checkNodeContent(networkPanel, "responseImageInfo", "2ms"); + checkNodeContent(networkPanel, "responseImageInfo", "16x16px"); + } + + // Check if the image is loaded already. + imgNode.addEventListener("load", function onLoad() { + imgNode.removeEventListener("load", onLoad, false); + checkImageResponseInfo(); + networkPanel.panel.hidePopup(); + testDriver.next(); + }, false); + yield undefined; + + // Check cached image request. + info("test 8: cached image request"); + httpActivity.response.httpVersion = "HTTP/1.1"; + httpActivity.response.status = 304; + httpActivity.response.statusText = "Not Modified"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: true, + requestFormData: false, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: true + }); + + imgNode = networkPanel.document.getElementById("responseImageCachedNode"); + is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64, + "Displayed image is correct"); + + networkPanel.panel.hidePopup(); + + // Test sent form data. + info("test 9: sent form data"); + httpActivity.request.postData.text = [ + "Content-Type: application/x-www-form-urlencoded", + "Content-Length: 59", + "name=rob&age=20" + ].join("\n"); + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: false, + requestFormData: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: true + }); + + checkNodeKeyValue(networkPanel, "requestFormDataContent", "name", "rob"); + checkNodeKeyValue(networkPanel, "requestFormDataContent", "age", "20"); + networkPanel.panel.hidePopup(); + + // Test no space after Content-Type: + info("test 10: no space after Content-Type header in post data"); + httpActivity.request.postData.text = "Content-Type:application/x-www-form-urlencoded\n"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: false, + requestFormData: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: true + }); + + networkPanel.panel.hidePopup(); + + // Test cached data. + + info("test 11: cached data"); + + httpActivity.request.url = TEST_ENCODING_ISO_8859_1; + httpActivity.response.headers[1].value = "application/json"; + httpActivity.response.content.mimeType = "application/json"; + httpActivity.response.content.text = "my cached data is here!"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: false, + requestFormData: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseBodyCached: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeContent(networkPanel, "responseBodyCachedContent", + "my cached data is here!"); + + networkPanel.panel.hidePopup(); + + // Test a response with a content type that can't be displayed in the + // NetworkPanel. + info("test 12: unknown content type"); + httpActivity.response.headers[1].value = "application/x-shockwave-flash"; + httpActivity.response.content.mimeType = "application/x-shockwave-flash"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: false, + requestFormData: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseBodyCached: false, + responseBodyUnknownType: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + let responseString = + WCU_l10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content", + ["application/x-shockwave-flash"]); + checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString); + networkPanel.panel.hidePopup(); + + /* + + // This test disabled. See bug 603620. + + // Test if the NetworkPanel figures out the content type based on an URL as + // well. + delete httpActivity.response.header["Content-Type"]; + httpActivity.url = "http://www.test.com/someCrazyFile.swf?done=right&ending=txt"; + + networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); + networkPanel.isDoneCallback = function NP_doneCallback() { + networkPanel.isDoneCallback = null; + testDriver.next(); + } + + yield undefined; + + checkIsVisible(networkPanel, { + requestBody: false, + requestFormData: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseBodyCached: false, + responseBodyUnknownType: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + // Systems without Flash installed will return an empty string here. Ignore. + if (networkPanel.document.getElementById("responseBodyUnknownTypeContent").textContent !== "") + checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString); + else + ok(true, "Flash not installed"); + + networkPanel.panel.hidePopup(); */ + + // All done! + testDriver = hud = null; + executeSoon(finishTest); + + yield undefined; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_notifications.js b/browser/devtools/webconsole/test/browser_webconsole_notifications.js new file mode 100644 index 000000000..c2be3ab8f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_notifications.js @@ -0,0 +1,77 @@ +/* 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/. */ + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for notifications"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let consoleOpened = promise.defer(); + let gotEvents = waitForEvents(consoleOpened.promise); + let hud = yield openConsole().then(() => consoleOpened.resolve()); + + yield gotEvents; +}); + +function waitForEvents(onConsoleOpened) { + let deferred = promise.defer(); + + function webConsoleCreated(aID) + { + Services.obs.removeObserver(observer, "web-console-created"); + ok(HUDService.getHudReferenceById(aID), "We have a hud reference"); + content.wrappedJSObject.console.log("adding a log message"); + } + + function webConsoleDestroyed(aID) + { + Services.obs.removeObserver(observer, "web-console-destroyed"); + ok(!HUDService.getHudReferenceById(aID), "We do not have a hud reference"); + executeSoon(deferred.resolve); + } + + function webConsoleMessage(aID, aNodeID) + { + Services.obs.removeObserver(observer, "web-console-message-created"); + ok(aID, "we have a console ID"); + is(typeof aNodeID, "string", "message node id is a string"); + onConsoleOpened.then(closeConsole); + } + + let observer = { + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function observe(aSubject, aTopic, aData) + { + aSubject = aSubject.QueryInterface(Ci.nsISupportsString); + + switch(aTopic) { + case "web-console-created": + webConsoleCreated(aSubject.data); + break; + case "web-console-destroyed": + webConsoleDestroyed(aSubject.data); + break; + case "web-console-message-created": + webConsoleMessage(aSubject, aData); + break; + default: + break; + } + }, + + init: function init() + { + Services.obs.addObserver(this, "web-console-created", false); + Services.obs.addObserver(this, "web-console-destroyed", false); + Services.obs.addObserver(this, "web-console-message-created", false); + } + } + + observer.init(); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_open-links-without-callback.js b/browser/devtools/webconsole/test/browser_webconsole_open-links-without-callback.js new file mode 100644 index 000000000..225a340ef --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_open-links-without-callback.js @@ -0,0 +1,52 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that if a link without an onclick callback is clicked the link is +// opened in a new tab and no exception occurs (bug 999236). + +"use strict"; + +function test() { + function* runner() { + const TEST_EVAL_STRING = "document"; + const TEST_PAGE_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + const {tab} = yield loadTab(TEST_PAGE_URI); + const hud = yield openConsole(tab); + + hud.jsterm.execute(TEST_EVAL_STRING); + + const EXPECTED_OUTPUT = new RegExp("HTMLDocument \.+"); + + let messages = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "JS eval output", + text: EXPECTED_OUTPUT, + category: CATEGORY_OUTPUT, + }], + }); + + let messageNode = messages[0].matched.values().next().value; + + // The correct anchor is second in the message node; the first anchor has + // class .cm-variable. Ignore the first one by not matching anchors that + // have the class .cm-variable. + let urlNode = messageNode.querySelector("a:not(.cm-variable)"); + + let linkOpened = false; + let oldOpenUILinkIn = window.openUILinkIn; + window.openUILinkIn = function(aLink) { + if (aLink == TEST_PAGE_URI) { + linkOpened = true; + } + } + + EventUtils.synthesizeMouseAtCenter(urlNode, {}, hud.iframeWindow); + + ok(linkOpened, "Clicking the URL opens the desired page"); + window.openUILinkIn = oldOpenUILinkIn; + } + + Task.spawn(runner).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_01.js b/browser/devtools/webconsole/test/browser_webconsole_output_01.js new file mode 100644 index 000000000..f9ec8ff38 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_01.js @@ -0,0 +1,125 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for various types of objects. + +const TEST_URI = "data:text/html;charset=utf8,test for console output - 01"; + +let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); + +let LONG_STRING_LENGTH = DebuggerServer.LONG_STRING_LENGTH; +let LONG_STRING_INITIAL_LENGTH = DebuggerServer.LONG_STRING_INITIAL_LENGTH; +DebuggerServer.LONG_STRING_LENGTH = 100; +DebuggerServer.LONG_STRING_INITIAL_LENGTH = 50; + +let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a"); +let initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + +let inputTests = [ + // 0 + { + input: "'hello \\nfrom \\rthe \\\"string world!'", + output: "\"hello \nfrom \rthe \"string world!\"", + }, + + // 1 + { + // unicode test + input: "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'", + output: "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"", + }, + + // 2 + { + input: "'" + longString + "'", + output: '"' + initialString + "\"[\u2026]", + printOutput: initialString, + }, + + // 3 + { + input: "''", + output: '""', + printOutput: '""', + }, + + // 4 + { + input: "0", + output: "0", + }, + + // 5 + { + input: "'0'", + output: '"0"', + }, + + // 6 + { + input: "42", + output: "42", + }, + + // 7 + { + input: "'42'", + output: '"42"', + }, + + // 8 + { + input: "/foobar/", + output: "/foobar/", + inspectable: true, + }, + + // 9 + { + input: "Symbol()", + output: "Symbol()" + }, + + // 10 + { + input: "Symbol('foo')", + output: "Symbol(foo)" + }, + + // 11 + { + input: "Symbol.iterator", + output: "Symbol(Symbol.iterator)" + }, +]; + +longString = initialString = null; + +function test() { + requestLongerTimeout(2); + + registerCleanupFunction(() => { + DebuggerServer.LONG_STRING_LENGTH = LONG_STRING_LENGTH; + DebuggerServer.LONG_STRING_INITIAL_LENGTH = LONG_STRING_INITIAL_LENGTH; + }); + + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + return checkOutputForInputs(hud, inputTests); + }).then(finishUp); +} + +function finishUp() { + longString = initialString = inputTests = null; + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_02.js b/browser/devtools/webconsole/test/browser_webconsole_output_02.js new file mode 100644 index 000000000..88c4df245 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_02.js @@ -0,0 +1,160 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the webconsole output for various types of objects. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-02.html"; + +let inputTests = [ + // 0 - native named function + { + input: "document.getElementById", + output: "function getElementById()", + printOutput: "function getElementById() {\n [native code]\n}", + inspectable: true, + variablesViewLabel: "getElementById()", + }, + + // 1 - anonymous function + { + input: "(function() { return 42; })", + output: "function ()", + printOutput: "function () { return 42; }", + inspectable: true, + }, + + // 2 - named function + { + input: "window.testfn1", + output: "function testfn1()", + printOutput: "function testfn1() { return 42; }", + inspectable: true, + variablesViewLabel: "testfn1()", + }, + + // 3 - anonymous function, but spidermonkey gives us an inferred name. + { + input: "testobj1.testfn2", + output: "function testobj1.testfn2()", + printOutput: "function () { return 42; }", + inspectable: true, + variablesViewLabel: "testobj1.testfn2()", + }, + + // 4 - named function with custom display name + { + input: "window.testfn3", + output: "function testfn3DisplayName()", + printOutput: "function testfn3() { return 42; }", + inspectable: true, + variablesViewLabel: "testfn3DisplayName()", + }, + + // 5 - basic array + { + input: "window.array1", + output: 'Array [ 1, 2, 3, "a", "b", "c", "4", "5" ]', + printOutput: "1,2,3,a,b,c,4,5", + inspectable: true, + variablesViewLabel: "Array[8]", + }, + + // 6 - array with objects + { + input: "window.array2", + output: 'Array [ "a", HTMLDocument \u2192 test-console-output-02.html, <body>, ' + + "DOMStringMap[0], DOMTokenList[0] ]", + printOutput: '"a,[object HTMLDocument],[object HTMLBodyElement],' + + '[object DOMStringMap],"', + inspectable: true, + variablesViewLabel: "Array[5]", + }, + + // 7 - array with more than 10 elements + { + input: "window.array3", + output: 'Array [ 1, Window \u2192 test-console-output-02.html, null, "a", "b", ' + + 'undefined, false, "", -Infinity, testfn3DisplayName(), 3 more\u2026 ]', + printOutput: '"1,[object Window],,a,b,,false,,-Infinity,' + + 'function testfn3() { return 42; },[object Object],foo,bar"', + inspectable: true, + variablesViewLabel: "Array[13]", + }, + + // 8 - array with holes and a cyclic reference + { + input: "window.array4", + output: 'Array [ <5 empty slots>, "test", Array[7] ]', + printOutput: '",,,,,test,"', + inspectable: true, + variablesViewLabel: "Array[7]", + }, + + // 9 + { + input: "window.typedarray1", + output: 'Int32Array [ 1, 287, 8651, 40983, 8754 ]', + printOutput: "[object Int32Array]", + inspectable: true, + variablesViewLabel: "Int32Array[5]", + }, + + // 10 - Set with cyclic reference + { + input: "window.set1", + output: 'Set [ 1, 2, null, Array[13], "a", "b", undefined, <head>, Set[9] ]', + printOutput: "[object Set]", + inspectable: true, + variablesViewLabel: "Set[9]", + }, + + // 11 - Object with cyclic reference and a getter + { + input: "window.testobj2", + output: 'Object { a: "b", c: "d", e: 1, f: "2", foo: Object, bar: Object, ' + + "getterTest: Getter }", + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 12 - Object with more than 10 properties + { + input: "window.testobj3", + output: 'Object { a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined, ' + + 'j: "", k: StyleSheetList[0], l: NodeList[5], 2 more\u2026 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 13 - Object with a non-enumerable property that we do not show + { + input: "window.testobj4", + output: 'Object { a: "b", c: "d", 1 more\u2026 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 14 - Map with cyclic references + { + input: "window.map1", + output: 'Map { a: "b", HTMLCollection[2]: Object, Map[3]: Set[9] }', + printOutput: "[object Map]", + inspectable: true, + variablesViewLabel: "Map[3]", + }, +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + inputTests = null; + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_03.js b/browser/devtools/webconsole/test/browser_webconsole_output_03.js new file mode 100644 index 000000000..40f8cf334 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_03.js @@ -0,0 +1,165 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the webconsole output for various types of objects. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-03.html"; + +let inputTests = [ + + // 0 + { + input: "document", + output: "HTMLDocument \u2192 " + TEST_URI, + printOutput: "[object HTMLDocument]", + inspectable: true, + noClick: true, + }, + + // 1 + { + input: "window", + output: "Window \u2192 " + TEST_URI, + printOutput: "[object Window", + inspectable: true, + noClick: true, + }, + + // 2 + { + input: "document.body", + output: "<body>", + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + }, + + // 3 + { + input: "document.body.dataset", + output: "DOMStringMap { }", + printOutput: "[object DOMStringMap]", + inspectable: true, + variablesViewLabel: "DOMStringMap[0]", + }, + + // 4 + { + input: "document.body.classList", + output: "DOMTokenList [ ]", + printOutput: '""', + inspectable: true, + variablesViewLabel: "DOMTokenList[0]", + }, + + // 5 + { + input: "window.location.href", + output: '"' + TEST_URI + '"', + noClick: true, + }, + + // 6 + { + input: "window.location", + output: "Location \u2192 " + TEST_URI, + printOutput: TEST_URI, + inspectable: true, + variablesViewLabel: "Location \u2192 test-console-output-03.html", + }, + + // 7 + { + input: "document.body.attributes", + output: "NamedNodeMap [ ]", + printOutput: "[object NamedNodeMap]", + inspectable: true, + variablesViewLabel: "NamedNodeMap[0]", + }, + + // 8 + { + input: "document.styleSheets", + output: "StyleSheetList [ ]", + printOutput: "[object StyleSheetList", + inspectable: true, + variablesViewLabel: "StyleSheetList[0]", + }, + + // 9 + { + input: "testBodyClassName()", + output: '<body class="test1 tezt2">', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + }, + + // 10 + { + input: "testBodyID()", + output: '<body class="test1 tezt2" id="foobarid">', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + }, + + // 11 + { + input: "document.body.classList", + output: 'DOMTokenList [ "test1", "tezt2" ]', + printOutput: '"test1 tezt2"', + inspectable: true, + variablesViewLabel: "DOMTokenList[2]", + }, + + // 12 + { + input: "testBodyDataset()", + output: '<body class="test1 tezt2" id="foobarid"' + + ' data-preview="zuzu"<a>foo">', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + }, + + // 13 + { + input: "document.body.dataset", + output: 'DOMStringMap { preview: "zuzu"<a>foo" }', + printOutput: "[object DOMStringMap]", + inspectable: true, + variablesViewLabel: "DOMStringMap[1]", + }, + + // 14 + { + input: "document.body.attributes", + output: 'NamedNodeMap [ class="test1 tezt2", id="foobarid", ' + + 'data-preview="zuzu"<a>foo" ]', + printOutput: "[object NamedNodeMap]", + inspectable: true, + variablesViewLabel: "NamedNodeMap[3]", + }, + + // 15 + { + input: "document.body.attributes[0]", + output: 'class="test1 tezt2"', + printOutput: "[object Attr]", + inspectable: true, + variablesViewLabel: 'class="test1 tezt2"', + }, +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + inputTests = null; + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_04.js b/browser/devtools/webconsole/test/browser_webconsole_output_04.js new file mode 100644 index 000000000..f50f4d073 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_04.js @@ -0,0 +1,127 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for various types of objects. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-04.html"; + +let inputTests = [ + // 0 + { + input: "testTextNode()", + output: '#text "hello world!"', + printOutput: "[object Text]", + inspectable: true, + noClick: true, + }, + + // 1 + { + input: "testCommentNode()", + output: /<!--\s+- Any copyright /, + printOutput: "[object Comment]", + inspectable: true, + noClick: true, + }, + + // 2 + { + input: "testDocumentFragment()", + output: 'DocumentFragment [ <div#foo1.bar>, <div#foo3> ]', + printOutput: "[object DocumentFragment]", + inspectable: true, + variablesViewLabel: "DocumentFragment[2]", + }, + + // 3 + { + input: "testError()", + output: "TypeError: window.foobar is not a function\n" + + "Stack trace:\n" + + "testError@" + TEST_URI + ":44", + printOutput: '"TypeError: window.foobar is not a function"', + inspectable: true, + variablesViewLabel: "TypeError", + }, + + // 4 + { + input: "testDOMException()", + output: 'DOMException [SyntaxError: "An invalid or illegal string was specified"', + printOutput: '"SyntaxError: An invalid or illegal string was specified"', + inspectable: true, + variablesViewLabel: "SyntaxError", + }, + + // 5 + { + input: "testCSSStyleDeclaration()", + output: 'CSS2Properties { color: "green", font-size: "2em" }', + printOutput: "[object CSS2Properties]", + inspectable: true, + noClick: true, + }, + + // 6 + { + input: "testStyleSheetList()", + output: "StyleSheetList [ CSSStyleSheet ]", + printOutput: "[object StyleSheetList", + inspectable: true, + variablesViewLabel: "StyleSheetList[1]", + }, + + // 7 + { + input: "document.styleSheets[0]", + output: "CSSStyleSheet", + printOutput: "[object CSSStyleSheet]", + inspectable: true, + }, + + // 8 + { + input: "document.styleSheets[0].cssRules", + output: "CSSRuleList [ CSSStyleRule, CSSMediaRule ]", + printOutput: "[object CSSRuleList", + inspectable: true, + variablesViewLabel: "CSSRuleList[2]", + }, + + // 9 + { + input: "document.styleSheets[0].cssRules[0]", + output: 'CSSStyleRule "p, div"', + printOutput: "[object CSSStyleRule", + inspectable: true, + variablesViewLabel: "CSSStyleRule", + }, + + // 10 + { + input: "document.styleSheets[0].cssRules[1]", + output: 'CSSMediaRule "print"', + printOutput: "[object CSSMediaRule", + inspectable: true, + variablesViewLabel: "CSSMediaRule", + }, +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + inputTests = null; + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_05.js b/browser/devtools/webconsole/test/browser_webconsole_output_05.js new file mode 100644 index 000000000..c985b7b34 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_05.js @@ -0,0 +1,130 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the webconsole output for various types of objects. + +const TEST_URI = "data:text/html;charset=utf8,test for console output - 05"; +const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + +let dateNow = Date.now(); + +let inputTests = [ + // 0 + { + input: "/foo?b*\\s\"ar/igym", + output: "/foo?b*\\s\"ar/gimy", + printOutput: "/foo?b*\\s\"ar/gimy", + inspectable: true, + }, + + // 1 + { + input: "null", + output: "null", + }, + + // 2 + { + input: "undefined", + output: "undefined", + }, + + // 3 + { + input: "true", + output: "true", + }, + + // 4 + { + input: "new Boolean(false)", + output: "false", + inspectable: true, + }, + + // 5 + { + input: "new Date(" + dateNow + ")", + output: "Date " + (new Date(dateNow)).toISOString(), + printOutput: (new Date(dateNow)).toString(), + inspectable: true, + }, + + // 6 + { + input: "new Date('test')", + output: "Invalid Date", + printOutput: "Invalid Date", + inspectable: true, + variablesViewLabel: "Invalid Date", + }, + + // 7 + { + input: "Date.prototype", + output: "Date", + printOutput: "Invalid Date", + inspectable: true, + variablesViewLabel: "Date", + }, + + // 8 + { + input: "new Number(43)", + output: "43", + inspectable: true, + }, + + // 9 + { + input: "new String('hello')", + output: 'String [ "h", "e", "l", "l", "o" ]', + printOutput: "hello", + inspectable: true, + variablesViewLabel: "String[5]" + }, + + // 9 + { + // XXX: Can't test fulfilled and rejected promises, because promises get + // settled on the next tick of the event loop. + input: "new Promise(function () {})", + output: 'Promise { <state>: "pending" }', + printOutput: "[object Promise]", + inspectable: true, + variablesViewLabel: "Promise" + }, + + // 10 + { + input: "(function () { var p = new Promise(function () {}); p.foo = 1; return p; }())", + output: 'Promise { <state>: "pending", foo: 1 }', + printOutput: "[object Promise]", + inspectable: true, + variablesViewLabel: "Promise" + }, + + //11 + { + input: "new Object({1: 'this\\nis\\nsupposed\\nto\\nbe\\na\\nvery\\nlong\\nstring\\n,shown\\non\\na\\nsingle\\nline', 2: 'a shorter string', 3: 100})", + output: 'Object { 1: "this is supposed to be a very long ' + ELLIPSIS + '", 2: "a shorter string", 3: 100 }', + printOutput: "[object Object]", + inspectable: false, + } +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + return checkOutputForInputs(hud, inputTests); + }).then(finishUp); +} + +function finishUp() { + inputTests = dateNow = null; + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_06.js b/browser/devtools/webconsole/test/browser_webconsole_output_06.js new file mode 100644 index 000000000..082b53dcc --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_06.js @@ -0,0 +1,127 @@ + /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + /* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + "use strict"; + +// Test the webconsole output for various arrays. + +const TEST_URI = "data:text/html;charset=utf8,test for console output - 06"; +const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; +const test_str_in = "SHOW\\nALL\\nOF\\nTHIS\\nON\\nA\\nSINGLE\\nLINE ONLY. ESCAPE ALL NEWLINE"; +const test_str_out = "SHOW ALL OF THIS ON A SINGLE LINE O" + ELLIPSIS; + +let inputTests = [ + // 1 - array with empty slots only + { + input: 'Array(5)', + output: 'Array [ <5 empty slots> ]', + printOutput: ',,,,', + inspectable: true, + variablesViewLabel: "Array[5]", + }, + // 2 - array with one empty slot at the beginning + { + input: '[,1,2,3]', + output: 'Array [ <1 empty slot>, 1, 2, 3 ]', + printOutput: ",1,2,3", + inspectable: true, + variablesViewLabel: "Array[4]", + }, + // 3 - array with multiple consecutive empty slots at the beginning + { + input: '[,,,3,4,5]', + output: 'Array [ <3 empty slots>, 3, 4, 5 ]', + printOutput: ",,,3,4,5", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + // 4 - array with one empty slot at the middle + { + input: '[0,1,,3,4,5]', + output: 'Array [ 0, 1, <1 empty slot>, 3, 4, 5 ]', + printOutput: "0,1,,3,4,5", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + // 5 - array with multiple successive empty slots at the middle + { + input: '[0,1,,,,5]', + output: 'Array [ 0, 1, <3 empty slots>, 5 ]', + printOutput: "0,1,,,,5", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + // 6 - array with multiple non successive single empty slots + { + input: '[0,,2,,4,5]', + output: 'Array [ 0, <1 empty slot>, 2, <1 empty slot>, 4, 5 ]', + printOutput: "0,,2,,4,5", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + // 7 - array with multiple multi-slot holes + { + input: '[0,,,3,,,,7,8]', + output: 'Array [ 0, <2 empty slots>, 3, <3 empty slots>, 7, 8 ]', + printOutput: "0,,,3,,,,7,8", + inspectable: true, + variablesViewLabel: "Array[9]", + }, + // 8 - array with a single slot hole at the end + { + input: '[0,1,2,3,4,,]', + output: 'Array [ 0, 1, 2, 3, 4, <1 empty slot> ]', + printOutput: "0,1,2,3,4,", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + // 9 - array with multiple consecutive empty slots at the end + { + input: '[0,1,2,,,,]', + output: 'Array [ 0, 1, 2, <3 empty slots> ]', + printOutput: "0,1,2,,,", + inspectable: true, + variablesViewLabel: "Array[6]", + }, + + // 10 - array with members explicitly set to null + { + input: '[0,null,null,3,4,5]', + output: 'Array [ 0, null, null, 3, 4, 5 ]', + printOutput: "0,,,3,4,5", + inspectable: true, + variablesViewLabel: "Array[6]" + }, + + // 11 - array with members explicitly set to undefined + { + input: '[0,undefined,undefined,3,4,5]', + output: 'Array [ 0, undefined, undefined, 3, 4, 5 ]', + printOutput: "0,,,3,4,5", + inspectable: true, + variablesViewLabel: "Array[6]" + }, + + //12 - array with long strings as elements + { + input: '["' + test_str_in + '", "' + test_str_in + '", "' + test_str_in + '"]', + output: 'Array [ "' + test_str_out + '", "' + test_str_out + '", "' + test_str_out + '" ]', + inspectable: false, + printOutput: "SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. ESCAPE ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. ESCAPE ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. ESCAPE ALL NEWLINE", + variablesViewLabel: "Array[3]" + } +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + return checkOutputForInputs(hud, inputTests); + }).then(finishUp); +} + +function finishUp() { + inputTests = null; + finishTest(); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_copy_newlines.js b/browser/devtools/webconsole/test/browser_webconsole_output_copy_newlines.js new file mode 100644 index 000000000..376099661 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_copy_newlines.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that multiple messages are copied into the clipboard and that they are +// separated by new lines. See bug 916997. + +"use strict"; + +let test = asyncTest(function*() { + const TEST_URI = "data:text/html;charset=utf8,<p>hello world, bug 916997"; + let clipboardValue = ""; + + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + let controller = top.document.commandDispatcher. + getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled"); + + content.console.log("Hello world! bug916997a"); + content.console.log("Hello world 2! bug916997b"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Hello world! bug916997a", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, { + text: "Hello world 2! bug916997b", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + hud.ui.output.selectAllMessages(); + hud.outputNode.focus(); + + goUpdateCommand("cmd_copy"); + controller = top.document.commandDispatcher.getControllerForCommand("cmd_copy"); + is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled"); + + let selection = hud.iframeWindow.getSelection() + ""; + info("selection '" + selection + "'"); + + waitForClipboard((str) => { + clipboardValue = str; + return str.indexOf("bug916997a") > -1 && str.indexOf("bug916997b") > -1; + }, + () => { goDoCommand("cmd_copy"); }, + () => { + info("clipboard value '" + clipboardValue + "'"); + let lines = clipboardValue.trim().split("\n"); + is(hud.outputNode.children.length, 2, "number of messages"); + is(lines.length, hud.outputNode.children.length, "number of lines"); + isnot(lines[0].indexOf("bug916997a"), -1, + "first message text includes 'bug916997a'"); + isnot(lines[1].indexOf("bug916997b"), -1, + "second message text includes 'bug916997b'"); + is(lines[0].indexOf("bug916997b"), -1, + "first message text does not include 'bug916997b'"); + }, + () => { + info("last clipboard value: '" + clipboardValue + "'"); + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js new file mode 100644 index 000000000..e7cc49369 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js @@ -0,0 +1,108 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejections should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed(null); +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.toolbox is null"); + +// Test the webconsole output for various types of DOM Nodes. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +let inputTests = [ + { + input: "testBodyNode()", + output: '<body id="body-id" class="body-class">', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocumentElement()", + output: '<html lang="en-US" dir="ltr">', + printOutput: "[object HTMLHtmlElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocument()", + output: 'HTMLDocument \u2192 ' + TEST_URI, + printOutput: "[object HTMLDocument]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testNode()", + output: '<p some-attribute="some-value">', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testNodeList()", + output: 'NodeList [ <html>, <head>, <meta>, <title>, <body#body-id.body-class>, <p>, <iframe>, <div.some.classname.here.with.more.classnames.here>, <script> ]', + printOutput: "[object NodeList]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testNodeInIframe()", + output: '<p>', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocumentFragment()", + output: 'DocumentFragment [ <span.foo>, <div#fragdiv> ]', + printOutput: "[object DocumentFragment]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testNodeInDocumentFragment()", + output: '<span class="foo" data-lolz="hehe">', + printOutput: "[object HTMLSpanElement]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testUnattachedNode()", + output: '<p class="such-class" data-data="such-data">', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: false + } +]; + +function test() { + requestLongerTimeout(2); + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js new file mode 100644 index 000000000..d6db04ea1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js @@ -0,0 +1,111 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the inspector links in the webconsole output for DOM Nodes actually +// open the inspector and select the right node + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +const TEST_DATA = [ + { + // The first test shouldn't be returning the body element as this is the + // default selected node, so re-selecting it won't fire the inspector-updated + // event + input: "testNode()", + output: '<p some-attribute="some-value">', + tagName: 'P', + attrs: [{name: "some-attribute", value: "some-value"}] + }, + { + input: "testBodyNode()", + output: '<body id="body-id" class="body-class">', + tagName: 'BODY', + attrs: [{name: "id", value: "body-id"}, {name: "class", value: "body-class"}] + }, + { + input: "testNodeInIframe()", + output: '<p>', + tagName: 'P', + attrs: [] + }, + { + input: "testDocumentElement()", + output: '<html lang="en-US" dir="ltr">', + tagName: 'HTML', + attrs: [{name: "lang", value: "en-US"}, {name: "dir", value: "ltr"}] + } +]; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + yield toolbox.selectTool("inspector"); + let inspector = toolbox.getCurrentPanel(); + yield toolbox.selectTool("webconsole"); + + info("Iterating over the test data"); + for (let data of TEST_DATA) { + let [result] = yield jsEval(data.input, hud, {text: data.output}); + let {widget, msg} = yield getWidgetAndMessage(result); + + let inspectorIcon = msg.querySelector(".open-inspector"); + ok(inspectorIcon, "Inspector icon found in the ElementNode widget"); + + info("Clicking on the inspector icon and waiting for the inspector to be selected"); + let onInspectorSelected = toolbox.once("inspector-selected"); + let onInspectorUpdated = inspector.once("inspector-updated"); + let onNewNode = toolbox.selection.once("new-node-front"); + + EventUtils.synthesizeMouseAtCenter(inspectorIcon, {}, + inspectorIcon.ownerDocument.defaultView); + yield onInspectorSelected; + yield onInspectorUpdated; + let nodeFront = yield onNewNode; + + ok(true, "Inspector selected and new node got selected"); + + is(nodeFront.tagName, data.tagName, "The correct node was highlighted"); + + let attrs = nodeFront.attributes; + for (let i in data.attrs) { + is(attrs[i].name, data.attrs[i].name, "The correct node was highlighted"); + is(attrs[i].value, data.attrs[i].value, "The correct node was highlighted"); + } + + info("Switching back to the console"); + yield toolbox.selectTool("webconsole"); + } + }).then(finishTest); +} + +function jsEval(input, hud, message) { + info("Executing '" + input + "' in the web console"); + + hud.jsterm.clearOutput(); + hud.jsterm.execute(input); + + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidgetAndMessage(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let widget = [...msg._messageObject.widgets][0]; + ok(widget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield widget.linkToInspector(); + + return {widget: widget, msg: msg}; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js new file mode 100644 index 000000000..cc420d492 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js @@ -0,0 +1,67 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that inspector links in webconsole outputs for DOM Nodes highlight +// the actual DOM Nodes on hover + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + yield toolbox.loadTool("inspector"); + let inspector = toolbox.getPanel("inspector"); + + info("Executing 'testNode()' in the web console to output a DOM Node"); + let [result] = yield jsEval("testNode()", hud, { + text: '<p some-attribute="some-value">' + }); + + let elementNodeWidget = yield getWidget(result); + + let nodeFront = yield hoverOverWidget(elementNodeWidget, toolbox); + let attrs = nodeFront.attributes; + is(nodeFront.tagName, "P", "The correct node was highlighted"); + is(attrs[0].name, "some-attribute", "The correct node was highlighted"); + is(attrs[0].value, "some-value", "The correct node was highlighted"); + }).then(finishTest); +} + +function jsEval(input, hud, message) { + hud.jsterm.execute(input); + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidget(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let elementNodeWidget = [...msg._messageObject.widgets][0]; + ok(elementNodeWidget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield elementNodeWidget.linkToInspector(); + + return elementNodeWidget; +} + +function* hoverOverWidget(widget, toolbox) { + info("Hovering over the output to highlight the node"); + + let onHighlight = toolbox.once("node-highlight"); + EventUtils.sendMouseEvent({type: "mouseover"}, widget.element, + widget.element.ownerDocument.defaultView); + let nodeFront = yield onHighlight; + ok(true, "The highlighter was shown on a node"); + return nodeFront; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js new file mode 100644 index 000000000..7cb0847e7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js @@ -0,0 +1,106 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that inspector links in the webconsole output for DOM Nodes do not try +// to highlight or select nodes once they have been detached + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +const TEST_DATA = [ + { + // The first test shouldn't be returning the body element as this is the + // default selected node, so re-selecting it won't fire the inspector-updated + // event + input: "testNode()", + output: '<p some-attribute="some-value">' + }, + { + input: "testBodyNode()", + output: '<body id="body-id" class="body-class">' + }, + { + input: "testNodeInIframe()", + output: '<p>' + }, + { + input: "testDocumentElement()", + output: '<html lang="en-US" dir="ltr">' + } +]; + +const PREF = "devtools.webconsole.persistlog"; + +function test() { + Services.prefs.setBoolPref(PREF, true); + registerCleanupFunction(() => Services.prefs.clearUserPref(PREF)); + + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + info("Executing the test data"); + let widgets = []; + for (let data of TEST_DATA) { + let [result] = yield jsEval(data.input, hud, {text: data.output}); + let {widget} = yield getWidgetAndMessage(result); + widgets.push(widget); + } + + info("Reloading the page"); + yield reloadPage(); + + info("Iterating over the ElementNode widgets"); + for (let widget of widgets) { + // Verify that openNodeInInspector rejects since the associated dom node + // doesn't exist anymore + yield widget.openNodeInInspector().then(() => { + ok(false, "The openNodeInInspector promise resolved"); + }, () => { + ok(true, "The openNodeInInspector promise rejected as expected"); + }); + yield toolbox.selectTool("webconsole"); + + // Verify that highlightDomNode rejects too, for the same reason + yield widget.highlightDomNode().then(() => { + ok(false, "The highlightDomNode promise resolved"); + }, () => { + ok(true, "The highlightDomNode promise rejected as expected"); + }); + } + }).then(finishTest); +} + +function jsEval(input, hud, message) { + info("Executing '" + input + "' in the web console"); + hud.jsterm.execute(input); + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidgetAndMessage(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let widget = [...msg._messageObject.widgets][0]; + ok(widget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield widget.linkToInspector(); + + return {widget: widget, msg: msg}; +} + +function reloadPage() { + let def = promise.defer(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + def.resolve(); + }, true); + content.location.reload(); + return def.promise; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_events.js b/browser/devtools/webconsole/test/browser_webconsole_output_events.js new file mode 100644 index 000000000..72b2ec93d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_events.js @@ -0,0 +1,53 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for DOM events. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-events.html"; + +let test = asyncTest(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + hud.jsterm.execute("testDOMEvents()"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "testDOMEvents() output", + text: "undefined", + category: CATEGORY_OUTPUT, + }], + }); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log() output for mousemove", + text: /"eventLogger" mousemove { target: .+, buttons: 0, clientX: \d+, clientY: \d+, layerX: \d+, layerY: \d+ }/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log() output for keypress", + text: /"eventLogger" keypress Shift { target: .+, key: .+, charCode: \d+, keyCode: \d+ }/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +});
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_order.js b/browser/devtools/webconsole/test/browser_webconsole_output_order.js new file mode 100644 index 000000000..4a2a9f6be --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js @@ -0,0 +1,47 @@ +/* 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 any output created from calls to the console API comes after the +// echoed JavaScript. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + let outputNode = jsterm.outputNode; + + jsterm.clearOutput(); + jsterm.execute("console.log('foo', 'bar');"); + + let [function_call, result, console_message] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "console.log('foo', 'bar');", + category: CATEGORY_INPUT, + }, + { + text: "undefined", + category: CATEGORY_OUTPUT, + }, + { + text: '"foo" "bar"', + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let fncall_node = [...function_call.matched][0]; + let result_node = [...result.matched][0]; + let console_message_node = [...console_message.matched][0]; + is(fncall_node.nextElementSibling, result_node, + "console.log() is followed by undefined"); + is(result_node.nextElementSibling, console_message_node, + "undefined is followed by 'foo' 'bar'"); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_table.js b/browser/devtools/webconsole/test/browser_webconsole_output_table.js new file mode 100644 index 000000000..512a3bc8f --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_table.js @@ -0,0 +1,158 @@ + /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + /* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that console.table() works as intended. + + "use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-table.html"; + +const TEST_DATA = [ + { + command: "console.table(languages1)", + data: [ + { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" }, + { _index: 1, name: "Object", fileExtension: "\".ts\"" }, + { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" } + ], + columns: { _index: "(index)", name: "name", fileExtension: "fileExtension" } + }, + { + command: "console.table(languages1, 'name')", + data: [ + { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" }, + { _index: 1, name: "Object", fileExtension: "\".ts\"" }, + { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" } + ], + columns: { _index: "(index)", name: "name" } + }, + { + command: "console.table(languages1, ['name'])", + data: [ + { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" }, + { _index: 1, name: "Object", fileExtension: "\".ts\"" }, + { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" } + ], + columns: { _index: "(index)", name: "name" } + }, + { + command: "console.table(languages2)", + data: [ + { _index: "csharp", name: "\"C#\"", paradigm: "\"object-oriented\"" }, + { _index: "fsharp", name: "\"F#\"", paradigm: "\"functional\"" } + ], + columns: { _index: "(index)", name: "name", paradigm: "paradigm" } + }, + { + command: "console.table([[1, 2], [3, 4]])", + data: [ + { _index: 0, 0: "1", 1: "2" }, + { _index: 1, 0: "3", 1: "4" } + ], + columns: { _index: "(index)", 0: "0", 1: "1" } + }, + { + command: "console.table({a: [1, 2], b: [3, 4]})", + data: [ + { _index: "a", 0: "1", 1: "2" }, + { _index: "b", 0: "3", 1: "4" } + ], + columns: { _index: "(index)", 0: "0", 1: "1" } + }, + { + command: "console.table(family)", + data: [ + { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"", age: "32" }, + { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"", age: "33" }, + { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"", age: "5" }, + { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" }, + ], + columns: { _index: "(index)", firstName: "firstName", lastName: "lastName", age: "age" } + }, + { + command: "console.table(family, [])", + data: [ + { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"", age: "32" }, + { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"", age: "33" }, + { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"", age: "5" }, + { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" }, + ], + columns: { _index: "(index)" } + }, + { + command: "console.table(family, ['firstName', 'lastName'])", + data: [ + { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"", age: "32" }, + { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"", age: "33" }, + { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"", age: "5" }, + { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" }, + ], + columns: { _index: "(index)", firstName: "firstName", lastName: "lastName" } + }, + { + command: "console.table(mySet)", + data: [ + { _index: 0, _value: "1" }, + { _index: 1, _value: "5" }, + { _index: 2, _value: "\"some text\"" }, + { _index: 3, _value: "null" }, + { _index: 4, _value: "undefined" } + ], + columns: { _index: "(iteration index)", _value: "Values" } + }, + { + command: "console.table(myMap)", + data: [ + { _index: 0, _key: "\"a string\"", _value: "\"value associated with 'a string'\"" }, + { _index: 1, _key: "5", _value: "\"value associated with 5\"" }, + ], + columns: { _index: "(iteration index)", _key: "Key", _value: "Values" } + } +]; + +add_task(function*() { + const {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + + for (let testdata of TEST_DATA) { + hud.jsterm.clearOutput(); + + info("Executing " + testdata.command); + + let onTableRender = once(hud.ui, "messages-table-rendered"); + hud.jsterm.execute(testdata.command); + yield onTableRender; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: testdata.command + " output", + consoleTable: true + }], + }); + + let node = [...result.matched][0]; + ok(node, "found trace log node"); + + let obj = node._messageObject; + ok(obj, "console.trace message object"); + + ok(obj._data, "found table data object"); + + let data = obj._data.map(entries => { + let result = {}; + + for (let key of Object.keys(entries)) { + result[key] = entries[key] instanceof HTMLElement ? + entries[key].textContent : entries[key]; + } + + return result; + }); + + is(data.toSource(), testdata.data.toSource(), "table data is correct"); + ok(obj._columns, "found table column object"); + is(obj._columns.toSource(), testdata.columns.toSource(), "table column is correct"); + } +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js new file mode 100644 index 000000000..14c054d36 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js @@ -0,0 +1,45 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests the property provider, which is part of the code completion +// infrastructure. + +const TEST_URI = "data:text/html;charset=utf8,<p>test the JS property provider"; + +function test() { + loadTab(TEST_URI).then(testPropertyProvider); +} + +function testPropertyProvider({browser}) { + browser.removeEventListener("load", testPropertyProvider, true); + let tools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; + let JSPropertyProvider = tools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider; + + let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + tmp.addDebuggerToGlobal(tmp); + let dbg = new tmp.Debugger; + let dbgWindow = dbg.makeGlobalObjectReference(content); + + let completion = JSPropertyProvider(dbgWindow, null, "thisIsNotDefined"); + is (completion.matches.length, 0, "no match for 'thisIsNotDefined"); + + // This is a case the PropertyProvider can't handle. Should return null. + completion = JSPropertyProvider(dbgWindow, null, "window[1].acb"); + is (completion, null, "no match for 'window[1].acb"); + + // A very advanced completion case. + var strComplete = + 'function a() { }document;document.getElementById(window.locatio'; + completion = JSPropertyProvider(dbgWindow, null, strComplete); + ok(completion.matches.length == 2, "two matches found"); + ok(completion.matchProp == "locatio", "matching part is 'test'"); + var matches = completion.matches; + matches.sort(); + ok(matches[0] == "location", "the first match is 'location'"); + ok(matches[1] == "locationbar", "the second match is 'locationbar'"); + + finishTest(); +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_reflow.js b/browser/devtools/webconsole/test/browser_webconsole_reflow.js new file mode 100644 index 000000000..328b296de --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_reflow.js @@ -0,0 +1,32 @@ +/* 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/. */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for reflow activity"; + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + function onReflowListenersReady(aType, aPacket) { + browser.contentDocument.body.style.display = "none"; + browser.contentDocument.body.clientTop; + } + + Services.prefs.setBoolPref("devtools.webconsole.filter.csslog", true); + hud.ui._updateReflowActivityListener(onReflowListenersReady); + Services.prefs.clearUserPref("devtools.webconsole.filter.csslog"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: /reflow: /, + category: CATEGORY_CSS, + severity: SEVERITY_LOG, + }], + }) +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js b/browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js new file mode 100644 index 000000000..c0899d868 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js @@ -0,0 +1,63 @@ +/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test Scratchpad panel linking</p>";
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Tools } = require("main");
+let { isTargetSupported } = Tools.scratchpad;
+
+Tools.scratchpad.isTargetSupported = () => true;
+
+add_task(function*() {
+ waitForExplicitFinish();
+ yield loadTab(TEST_URI);
+
+ info("Opening toolbox with Scratchpad panel");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "scratchpad", "window");
+
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ let { scratchpad } = scratchpadPanel;
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Scratchpad is currently selected panel");
+
+ info("Switching to webconsole panel");
+
+ let webconsolePanel = yield toolbox.selectTool("webconsole");
+ let { hud } = webconsolePanel;
+ is(toolbox.getCurrentPanel(), webconsolePanel,
+ "Webconsole is currently selected panel");
+
+ info("console.log()ing from Scratchpad");
+
+ scratchpad.setText("console.log('foobar-from-scratchpad')");
+ scratchpad.run();
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{ text: "foobar-from-scratchpad" }]
+ });
+
+ info("Clicking link to switch to and focus Scratchpad");
+
+ let [matched] = [...messages[0].matched];
+ ok(matched, "Found logged message from Scratchpad");
+ let anchor = matched.querySelector("a.message-location");
+
+ toolbox.on("scratchpad-selected", function selected() {
+ toolbox.off("scratchpad-selected", selected);
+
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Clicking link switches to Scratchpad panel");
+
+ is(Services.ww.activeWindow, toolbox.frame.ownerGlobal,
+ "Scratchpad's toolbox is focused");
+
+ Tools.scratchpad.isTargetSupported = isTargetSupported;
+ finish();
+ });
+
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+});
diff --git a/browser/devtools/webconsole/test/browser_webconsole_show_subresource_security_errors.js b/browser/devtools/webconsole/test/browser_webconsole_show_subresource_security_errors.js new file mode 100644 index 000000000..282c8aab1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_show_subresource_security_errors.js @@ -0,0 +1,30 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Ensure non-toplevel security errors are displayed + +const TEST_URI = "data:text/html;charset=utf8,Web Console subresource STS warning test"; +const TEST_DOC = "https://example.com/browser/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.html"; +const SAMPLE_MSG = 'invalid Strict-Transport-Security header' + +let test = asyncTest(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + content.location = TEST_DOC; + yield loaded; + + yield waitForSuccess({ + name: "Subresource STS warning displayed successfully", + validator: function() { + return hud.outputNode.textContent.indexOf(SAMPLE_MSG) > -1; + } + }); +}); diff --git a/browser/devtools/webconsole/test/browser_webconsole_split.js b/browser/devtools/webconsole/test/browser_webconsole_split.js new file mode 100644 index 000000000..9501cd238 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_split.js @@ -0,0 +1,247 @@ +/* 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/. */ + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for splitting"; + +function test() +{ + // Test is slow on Linux EC2 instances - Bug 962931 + requestLongerTimeout(2); + + let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); + let Toolbox = devtools.Toolbox; + let toolbox; + + loadTab(TEST_URI).then(testConsoleLoadOnDifferentPanel); + + function testConsoleLoadOnDifferentPanel() + { + info("About to check console loads even when non-webconsole panel is open"); + + openPanel("inspector").then(() => { + toolbox.on("webconsole-ready", () => { + ok(true, "Webconsole has been triggered as loaded while another tool is active"); + testKeyboardShortcuts(); + }); + + // Opens split console. + toolbox.toggleSplitConsole(); + }); + } + + function testKeyboardShortcuts() + { + info("About to check that panel responds to ESCAPE keyboard shortcut"); + + toolbox.once("split-console", () => { + ok(true, "Split console has been triggered via ESCAPE keypress"); + checkAllTools(); + }); + + // Closes split console. + EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow); + } + + function checkAllTools() + { + info("About to check split console with each panel individually."); + + Task.spawn(function() { + yield openAndCheckPanel("jsdebugger"); + yield openAndCheckPanel("inspector"); + yield openAndCheckPanel("styleeditor"); + yield openAndCheckPanel("jsprofiler"); + yield openAndCheckPanel("netmonitor"); + + yield checkWebconsolePanelOpened(); + testBottomHost(); + }); + } + + function getCurrentUIState() + { + let win = toolbox.doc.defaultView; + let deck = toolbox.doc.querySelector("#toolbox-deck"); + let webconsolePanel = toolbox.webconsolePanel; + let splitter = toolbox.doc.querySelector("#toolbox-console-splitter"); + + let containerHeight = parseFloat(win.getComputedStyle(deck.parentNode).getPropertyValue("height")); + let deckHeight = parseFloat(win.getComputedStyle(deck).getPropertyValue("height")); + let webconsoleHeight = parseFloat(win.getComputedStyle(webconsolePanel).getPropertyValue("height")); + let splitterVisibility = !splitter.getAttribute("hidden"); + let openedConsolePanel = toolbox.currentToolId === "webconsole"; + let cmdButton = toolbox.doc.querySelector("#command-button-splitconsole"); + + return { + deckHeight: deckHeight, + containerHeight: containerHeight, + webconsoleHeight: webconsoleHeight, + splitterVisibility: splitterVisibility, + openedConsolePanel: openedConsolePanel, + buttonSelected: cmdButton.hasAttribute("checked") + }; + } + + function checkWebconsolePanelOpened() + { + info("About to check special cases when webconsole panel is open."); + + let deferred = promise.defer(); + + // Start with console split, so we can test for transition to main panel. + toolbox.toggleSplitConsole(); + + let currentUIState = getCurrentUIState(); + + ok (currentUIState.splitterVisibility, "Splitter is visible when console is split"); + ok (currentUIState.deckHeight > 0, "Deck has a height > 0 when console is split"); + ok (currentUIState.webconsoleHeight > 0, "Web console has a height > 0 when console is split"); + ok (!currentUIState.openedConsolePanel, "The console panel is not the current tool"); + ok (currentUIState.buttonSelected, "The command button is selected"); + + openPanel("webconsole").then(() => { + + let currentUIState = getCurrentUIState(); + + ok (!currentUIState.splitterVisibility, "Splitter is hidden when console is opened."); + is (currentUIState.deckHeight, 0, "Deck has a height == 0 when console is opened."); + is (currentUIState.webconsoleHeight, currentUIState.containerHeight, "Web console is full height."); + ok (currentUIState.openedConsolePanel, "The console panel is the current tool"); + ok (currentUIState.buttonSelected, "The command button is still selected."); + + // Make sure splitting console does nothing while webconsole is opened + toolbox.toggleSplitConsole(); + + currentUIState = getCurrentUIState(); + + ok (!currentUIState.splitterVisibility, "Splitter is hidden when console is opened."); + is (currentUIState.deckHeight, 0, "Deck has a height == 0 when console is opened."); + is (currentUIState.webconsoleHeight, currentUIState.containerHeight, "Web console is full height."); + ok (currentUIState.openedConsolePanel, "The console panel is the current tool"); + ok (currentUIState.buttonSelected, "The command button is still selected."); + + // Make sure that split state is saved after opening another panel + openPanel("inspector").then(() => { + let currentUIState = getCurrentUIState(); + ok (currentUIState.splitterVisibility, "Splitter is visible when console is split"); + ok (currentUIState.deckHeight > 0, "Deck has a height > 0 when console is split"); + ok (currentUIState.webconsoleHeight > 0, "Web console has a height > 0 when console is split"); + ok (!currentUIState.openedConsolePanel, "The console panel is not the current tool"); + ok (currentUIState.buttonSelected, "The command button is still selected."); + + toolbox.toggleSplitConsole(); + deferred.resolve(); + + }); + }); + return deferred.promise; + } + + function openPanel(toolId, callback) + { + let deferred = promise.defer(); + let target = TargetFactory.forTab(gBrowser.selectedTab); + gDevTools.showToolbox(target, toolId).then(function(box) { + toolbox = box; + deferred.resolve(); + }).then(null, console.error); + return deferred.promise; + } + + function openAndCheckPanel(toolId) + { + let deferred = promise.defer(); + openPanel(toolId).then(() => { + info ("Checking toolbox for " + toolId); + checkToolboxUI(toolbox.getCurrentPanel()); + deferred.resolve(); + }); + return deferred.promise; + } + + function checkToolboxUI() + { + let currentUIState = getCurrentUIState(); + + ok (!currentUIState.splitterVisibility, "Splitter is hidden by default"); + is (currentUIState.deckHeight, currentUIState.containerHeight, "Deck has a height > 0 by default"); + is (currentUIState.webconsoleHeight, 0, "Web console is collapsed by default"); + ok (!currentUIState.openedConsolePanel, "The console panel is not the current tool"); + ok (!currentUIState.buttonSelected, "The command button is not selected."); + + toolbox.toggleSplitConsole(); + + currentUIState = getCurrentUIState(); + + ok (currentUIState.splitterVisibility, "Splitter is visible when console is split"); + ok (currentUIState.deckHeight > 0, "Deck has a height > 0 when console is split"); + ok (currentUIState.webconsoleHeight > 0, "Web console has a height > 0 when console is split"); + is (Math.round(currentUIState.deckHeight + currentUIState.webconsoleHeight), + currentUIState.containerHeight, + "Everything adds up to container height"); + ok (!currentUIState.openedConsolePanel, "The console panel is not the current tool"); + ok (currentUIState.buttonSelected, "The command button is selected."); + + toolbox.toggleSplitConsole(); + + currentUIState = getCurrentUIState(); + + ok (!currentUIState.splitterVisibility, "Splitter is hidden after toggling"); + is (currentUIState.deckHeight, currentUIState.containerHeight, "Deck has a height > 0 after toggling"); + is (currentUIState.webconsoleHeight, 0, "Web console is collapsed after toggling"); + ok (!currentUIState.openedConsolePanel, "The console panel is not the current tool"); + ok (!currentUIState.buttonSelected, "The command button is not selected."); + } + + function testBottomHost() + { + checkHostType(Toolbox.HostType.BOTTOM); + + checkToolboxUI(); + + toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost); + } + + function testSidebarHost() + { + checkHostType(Toolbox.HostType.SIDE); + + checkToolboxUI(); + + toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost); + } + + function testWindowHost() + { + checkHostType(Toolbox.HostType.WINDOW); + + checkToolboxUI(); + + toolbox.switchHost(Toolbox.HostType.BOTTOM).then(testDestroy); + } + + function checkHostType(hostType) + { + is(toolbox.hostType, hostType, "host type is " + hostType); + + let pref = Services.prefs.getCharPref("devtools.toolbox.host"); + is(pref, hostType, "host pref is " + hostType); + } + + function testDestroy() + { + toolbox.destroy().then(function() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + gDevTools.showToolbox(target).then(finish); + }); + } + + function finish() + { + toolbox = null; + finishTest(); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_split_escape_key.js b/browser/devtools/webconsole/test/browser_webconsole_split_escape_key.js new file mode 100644 index 000000000..2b8b8e4e7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_split_escape_key.js @@ -0,0 +1,171 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function test() { + info("Test various cases where the escape key should hide the split console."); + + let toolbox; + let hud; + let jsterm; + let hudMessages; + let variablesView; + + Task.spawn(runner).then(finish); + + function* runner() { + let {tab} = yield loadTab("data:text/html;charset=utf-8,<p>Web Console test for splitting"); + let target = TargetFactory.forTab(tab); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + + yield testCreateSplitConsoleAfterEscape(); + + yield showAutoCompletePopoup(); + + yield testHideAutoCompletePopupAfterEscape(); + + yield executeJS(); + yield clickMessageAndShowVariablesView(); + jsterm.inputNode.focus(); + + yield testHideVariablesViewAfterEscape(); + + yield clickMessageAndShowVariablesView(); + yield startPropertyEditor(); + + yield testCancelPropertyEditorAfterEscape(); + yield testHideVariablesViewAfterEscape(); + yield testHideSplitConsoleAfterEscape(); + } + + function testCreateSplitConsoleAfterEscape() { + let result = toolbox.once("webconsole-ready", () => { + hud = toolbox.getPanel("webconsole").hud; + jsterm = hud.jsterm; + ok(toolbox.splitConsole, "Split console is created."); + }); + + let contentWindow = toolbox.frame.contentWindow; + contentWindow.focus(); + EventUtils.sendKey("ESCAPE", contentWindow); + + return result; + } + + function testShowSplitConsoleAfterEscape() { + let result = toolbox.once("split-console", () => { + ok(toolbox.splitConsole, "Split console is shown."); + }); + EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow); + + return result; + } + + function testHideSplitConsoleAfterEscape() { + let result = toolbox.once("split-console", () => { + ok(!toolbox.splitConsole, "Split console is hidden."); + }); + EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow); + + return result; + } + + function testHideVariablesViewAfterEscape() { + let result = jsterm.once("sidebar-closed", () => { + ok(!hud.ui.jsterm.sidebar, + "Variables view is hidden."); + ok(toolbox.splitConsole, + "Split console is open after hiding the variables view."); + }); + EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow); + + return result; + } + + function testHideAutoCompletePopupAfterEscape() { + let deferred = promise.defer(); + let popup = jsterm.autocompletePopup; + + popup._panel.addEventListener("popuphidden", function popupHidden() { + popup._panel.removeEventListener("popuphidden", popupHidden, false); + ok(!popup.isOpen, + "Auto complete popup is hidden."); + ok(toolbox.splitConsole, + "Split console is open after hiding the autocomplete popup."); + + deferred.resolve(); + }, false); + + EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow); + + return deferred.promise; + } + + function testCancelPropertyEditorAfterEscape() { + EventUtils.sendKey("ESCAPE", variablesView.window); + ok(hud.ui.jsterm.sidebar, + "Variables view is open after canceling property editor."); + ok(toolbox.splitConsole, + "Split console is open after editing."); + } + + function executeJS() { + jsterm.execute("var foo = { bar: \"baz\" }; foo;"); + hudMessages = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Object { bar: \"baz\" }", + category: CATEGORY_OUTPUT, + objects: true + }], + }); + } + + function clickMessageAndShowVariablesView() { + let result = jsterm.once("variablesview-fetched", (event, vview) => { + variablesView = vview; + }); + + let clickable = hudMessages[0].clickableElements[0]; + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); + + return result; + } + + function startPropertyEditor() { + let results = yield findVariableViewProperties(variablesView, [ + {name: "bar", value: "baz"} + ], {webconsole: hud}); + results[0].matchedProp.focus(); + EventUtils.synthesizeKey("VK_RETURN", variablesView.window); + } + + function showAutoCompletePopoup() { + let deferred = promise.defer(); + let popupPanel = jsterm.autocompletePopup._panel; + + popupPanel.addEventListener("popupshown", function popupShown() { + popupPanel.removeEventListener("popupshown", popupShown, false); + deferred.resolve(); + }, false); + + jsterm.inputNode.focus(); + jsterm.setInputValue("document.location."); + EventUtils.sendKey("TAB", hud.iframeWindow); + + return deferred.promise; + } + + function finish() { + toolbox.destroy().then(() => { + toolbox = null; + hud = null; + jsterm = null; + hudMessages = null; + variablesView = null; + + finishTest(); + }); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_split_focus.js b/browser/devtools/webconsole/test/browser_webconsole_split_focus.js new file mode 100644 index 000000000..51d7b9a79 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_split_focus.js @@ -0,0 +1,74 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function test() { + info("Test that the split console state is persisted"); + + let toolbox; + let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for splitting</p>"; + + Task.spawn(runner).then(finish); + + function* runner() { + info("Opening a tab while there is no user setting on split console pref"); + let {tab} = yield loadTab(TEST_URI); + let target = TargetFactory.forTab(tab); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + + ok(!toolbox.splitConsole, "Split console is hidden by default"); + + info ("Focusing the search box before opening the split console"); + let inspector = toolbox.getPanel("inspector"); + inspector.searchBox.focus(); + + // Use the binding element since inspector.searchBox is a XUL element. + let activeElement = getActiveElement(inspector.panelDoc); + activeElement = activeElement.ownerDocument.getBindingParent(activeElement); + is (activeElement, inspector.searchBox, "Search box is focused"); + + yield toolbox.openSplitConsole(); + + ok(toolbox.splitConsole, "Split console is now visible"); + + // Use the binding element since jsterm.inputNode is a XUL textarea element. + activeElement = getActiveElement(toolbox.doc); + activeElement = activeElement.ownerDocument.getBindingParent(activeElement); + let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode; + is(activeElement, inputNode, "Split console input is focused by default"); + + yield toolbox.closeSplitConsole(); + + info ("Making sure that the search box is refocused after closing the split console"); + // Use the binding element since inspector.searchBox is a XUL element. + activeElement = getActiveElement(inspector.panelDoc); + activeElement = activeElement.ownerDocument.getBindingParent(activeElement); + is (activeElement, inspector.searchBox, "Search box is focused"); + + yield toolbox.destroy(); + } + + function getActiveElement(doc) { + let activeElement = doc.activeElement; + while (activeElement && activeElement.contentDocument) { + activeElement = activeElement.contentDocument.activeElement; + } + return activeElement; + } + + function toggleSplitConsoleWithEscape() { + let onceSplitConsole = toolbox.once("split-console"); + let contentWindow = toolbox.frame.contentWindow; + contentWindow.focus(); + EventUtils.sendKey("ESCAPE", contentWindow); + return onceSplitConsole; + } + + function finish() { + toolbox = TEST_URI = null; + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight"); + finishTest(); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_split_persist.js b/browser/devtools/webconsole/test/browser_webconsole_split_persist.js new file mode 100644 index 000000000..d9116f2b1 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_split_persist.js @@ -0,0 +1,111 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function test() { + info("Test that the split console state is persisted"); + + let toolbox; + let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for splitting</p>"; + + Task.spawn(runner).then(finish); + + function* runner() { + info("Opening a tab while there is no user setting on split console pref"); + let {tab} = yield loadTab(TEST_URI); + let target = TargetFactory.forTab(tab); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + + ok(!toolbox.splitConsole, "Split console is hidden by default."); + ok(!isCommandButtonChecked(), "Split console button is unchecked by default."); + yield toggleSplitConsoleWithEscape(); + ok(toolbox.splitConsole, "Split console is now visible."); + ok(isCommandButtonChecked(), "Split console button is now checked."); + ok(getVisiblePrefValue(), "Visibility pref is true"); + + is(getHeightPrefValue(), toolbox.webconsolePanel.height, "Panel height matches the pref"); + toolbox.webconsolePanel.height = 200; + + yield toolbox.destroy(); + + info("Opening a tab while there is a true user setting on split console pref"); + ({tab} = yield loadTab(TEST_URI)); + target = TargetFactory.forTab(tab); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + + ok(toolbox.splitConsole, "Split console is visible by default."); + ok(isCommandButtonChecked(), "Split console button is checked by default."); + is(getHeightPrefValue(), 200, "Height is set based on panel height after closing"); + + // Use the binding element since jsterm.inputNode is a XUL textarea element. + let activeElement = getActiveElement(toolbox.doc); + activeElement = activeElement.ownerDocument.getBindingParent(activeElement); + let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode; + is(activeElement, inputNode, "Split console input is focused by default"); + + toolbox.webconsolePanel.height = 1; + ok (toolbox.webconsolePanel.clientHeight > 1, + "The actual height of the console is bound with a min height"); + + toolbox.webconsolePanel.height = 10000; + ok (toolbox.webconsolePanel.clientHeight < 10000, + "The actual height of the console is bound with a max height"); + + yield toggleSplitConsoleWithEscape(); + ok(!toolbox.splitConsole, "Split console is now hidden."); + ok(!isCommandButtonChecked(), "Split console button is now unchecked."); + ok(!getVisiblePrefValue(), "Visibility pref is false"); + + yield toolbox.destroy(); + + is(getHeightPrefValue(), 10000, "Height is set based on panel height after closing"); + + + info("Opening a tab while there is a false user setting on split console pref"); + ({tab} = yield loadTab(TEST_URI)); + target = TargetFactory.forTab(tab); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + + ok(!toolbox.splitConsole, "Split console is hidden by default."); + ok(!getVisiblePrefValue(), "Visibility pref is false"); + + yield toolbox.destroy(); + } + + function getActiveElement(doc) { + let activeElement = doc.activeElement; + while (activeElement && activeElement.contentDocument) { + activeElement = activeElement.contentDocument.activeElement; + } + return activeElement; + } + + function getVisiblePrefValue() { + return Services.prefs.getBoolPref("devtools.toolbox.splitconsoleEnabled"); + } + + function getHeightPrefValue() { + return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight"); + } + + function isCommandButtonChecked() { + return toolbox.doc.querySelector("#command-button-splitconsole"). + hasAttribute("checked"); + } + + function toggleSplitConsoleWithEscape() { + let onceSplitConsole = toolbox.once("split-console"); + let contentWindow = toolbox.frame.contentWindow; + contentWindow.focus(); + EventUtils.sendKey("ESCAPE", contentWindow); + return onceSplitConsole; + } + + function finish() { + toolbox = TEST_URI = null; + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight"); + finishTest(); + } +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_start_netmon_first.js b/browser/devtools/webconsole/test/browser_webconsole_start_netmon_first.js new file mode 100644 index 000000000..b648da021 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_start_netmon_first.js @@ -0,0 +1,37 @@ +/* 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/. */ + +// Check that the webconsole works if the network monitor is first opened, then +// the user switches to the webconsole. See bug 970914. + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + + const target = TargetFactory.forTab(tab); + const toolbox = yield gDevTools.showToolbox(target, "netmonitor"); + + const hud = yield openConsole(tab); + + hud.jsterm.execute("console.log('foobar bug970914')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log", + text: "foobar bug970914", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let text = hud.outputNode.textContent; + isnot(text.indexOf("foobar bug970914"), -1, "console.log message confirmed"); + ok(!/logging API|disabled by a script/i.test(text), + "no warning about disabled console API"); + } +} + diff --git a/browser/devtools/webconsole/test/browser_webconsole_view_source.js b/browser/devtools/webconsole/test/browser_webconsole_view_source.js new file mode 100644 index 000000000..ad008478d --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that source URLs in the Web Console can be clicked to display the +// standard View Source window. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html"; + +let getItemForAttachment; +let Sources; +let getItemInvoked = false; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole(null).then(testViewSource); + }); +} + +function testViewSource(hud) { + info("console opened"); + + let button = content.document.querySelector("button"); + ok(button, "we have the button on the page"); + + expectUncaughtException(); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + + openDebugger().then(({panelWin: { DebuggerView }}) => { + info("debugger opened"); + Sources = DebuggerView.Sources; + openConsole().then((hud) => { + info("console opened again"); + + waitForMessages({ + webconsole: hud, + messages: [{ + text: "fooBazBaz is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }).then(onMessage); + }); + }); + + function onMessage([result]) { + let msg = [...result.matched][0]; + ok(msg, "error message"); + let locationNode = msg.querySelector(".message-location"); + ok(locationNode, "location node"); + + Services.ww.registerNotification(observer); + + getItemForAttachment = Sources.getItemForAttachment; + Sources.getItemForAttachment = () => { + getItemInvoked = true; + return false; + }; + + EventUtils.sendMouseEvent({ type: "click" }, locationNode); + } +} + +let observer = { + observe: function(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") { + return; + } + + ok(true, "the view source window was opened in response to clicking " + + "the location node"); + + aSubject.close(); + ok(getItemInvoked, "custom getItemForAttachment() was invoked"); + Sources.getItemForAttachment = getItemForAttachment; + Sources = getItemForAttachment = null; + finishTest(); + } +}; + +registerCleanupFunction(function() { + Services.ww.unregisterNotification(observer); +}); diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js new file mode 100644 index 000000000..c49badb40 --- /dev/null +++ b/browser/devtools/webconsole/test/head.js @@ -0,0 +1,1677 @@ +/* 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/. */ + +"use strict"; + +let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let {require, TargetFactory} = devtools; +let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils"); +let {Messages} = require("devtools/webconsole/console-output"); + +// promise._reportErrors = true; // please never leave me. +//Services.prefs.setBoolPref("devtools.debugger.log", true); + +let gPendingOutputTest = 0; + +// The various categories of messages. +const CATEGORY_NETWORK = 0; +const CATEGORY_CSS = 1; +const CATEGORY_JS = 2; +const CATEGORY_WEBDEV = 3; +const CATEGORY_INPUT = 4; +const CATEGORY_OUTPUT = 5; +const CATEGORY_SECURITY = 6; + +// The possible message severities. +const SEVERITY_ERROR = 0; +const SEVERITY_WARNING = 1; +const SEVERITY_INFO = 2; +const SEVERITY_LOG = 3; + +// The indent of a console group in pixels. +const GROUP_INDENT = 12; + +const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); + +gDevTools.testing = true; + +function asyncTest(generator) { + return () => { + Task.spawn(generator).then(finishTest); + }; +} + + +function loadTab(url) { + let deferred = promise.defer(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let browser = gBrowser.getBrowserForTab(tab); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve({tab: tab, browser: browser}); + }, true); + + return deferred.promise; +} + +function loadBrowser(browser) { + let deferred = promise.defer(); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve(null); + }, true); + + return deferred.promise; +} + +function closeTab(tab) { + let deferred = promise.defer(); + + let container = gBrowser.tabContainer; + + container.addEventListener("TabClose", function onTabClose() { + container.removeEventListener("TabClose", onTabClose, true); + deferred.resolve(null); + }, true); + + gBrowser.removeTab(tab); + + return deferred.promise; +} + +function afterAllTabsLoaded(callback, win) { + win = win || window; + + let stillToLoad = 0; + + function onLoad() { + this.removeEventListener("load", onLoad, true); + stillToLoad--; + if (!stillToLoad) + callback(); + } + + for (let a = 0; a < win.gBrowser.tabs.length; a++) { + let browser = win.gBrowser.tabs[a].linkedBrowser; + if (browser.webProgress.isLoadingDocument) { + stillToLoad++; + browser.addEventListener("load", onLoad, true); + } + } + + if (!stillToLoad) + callback(); +} + +/** + * Check if a log entry exists in the HUD output node. + * + * @param {Element} aOutputNode + * the HUD output node. + * @param {string} aMatchString + * the string you want to check if it exists in the output node. + * @param {string} aMsg + * the message describing the test + * @param {boolean} [aOnlyVisible=false] + * find only messages that are visible, not hidden by the filter. + * @param {boolean} [aFailIfFound=false] + * fail the test if the string is found in the output node. + * @param {string} aClass [optional] + * find only messages with the given CSS class. + */ +function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, + aFailIfFound, aClass) +{ + let selector = ".message"; + // Skip entries that are hidden by the filter. + if (aOnlyVisible) { + selector += ":not(.filtered-by-type):not(.filtered-by-string)"; + } + if (aClass) { + selector += "." + aClass; + } + + let msgs = aOutputNode.querySelectorAll(selector); + let found = false; + for (let i = 0, n = msgs.length; i < n; i++) { + let message = msgs[i].textContent.indexOf(aMatchString); + if (message > -1) { + found = true; + break; + } + } + + is(found, !aFailIfFound, aMsg); +} + +/** + * A convenience method to call testLogEntry(). + * + * @param string aString + * The string to find. + */ +function findLogEntry(aString) +{ + testLogEntry(outputNode, aString, "found " + aString); +} + +/** + * Open the Web Console for the given tab. + * + * @param nsIDOMElement [aTab] + * Optional tab element for which you want open the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [aCallback] + * Optional function to invoke after the Web Console completes + * initialization (web-console-created). + * @return object + * A promise that is resolved once the web console is open. + */ +let openConsole = function(aTab) { + let webconsoleOpened = promise.defer(); + let target = TargetFactory.forTab(aTab || gBrowser.selectedTab); + gDevTools.showToolbox(target, "webconsole").then(toolbox => { + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm._lazyVariablesView = false; + webconsoleOpened.resolve(hud); + }); + return webconsoleOpened.promise; +}; + +/** + * Close the Web Console for the given tab. + * + * @param nsIDOMElement [aTab] + * Optional tab element for which you want close the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [aCallback] + * Optional function to invoke after the Web Console completes + * closing (web-console-destroyed). + * @return object + * A promise that is resolved once the web console is closed. + */ +let closeConsole = Task.async(function* (aTab) { + let target = TargetFactory.forTab(aTab || gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + if (toolbox) { + yield toolbox.destroy(); + } +}); + +/** + * Wait for a context menu popup to open. + * + * @param nsIDOMElement aPopup + * The XUL popup you expect to open. + * @param nsIDOMElement aButton + * The button/element that receives the contextmenu event. This is + * expected to open the popup. + * @param function aOnShown + * Function to invoke on popupshown event. + * @param function aOnHidden + * Function to invoke on popuphidden event. + * @return object + * A Promise object that is resolved after the popuphidden event + * callback is invoked. + */ +function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden) +{ + function onPopupShown() { + info("onPopupShown"); + aPopup.removeEventListener("popupshown", onPopupShown); + + aOnShown && aOnShown(); + + // Use executeSoon() to get out of the popupshown event. + aPopup.addEventListener("popuphidden", onPopupHidden); + executeSoon(() => aPopup.hidePopup()); + } + function onPopupHidden() { + info("onPopupHidden"); + aPopup.removeEventListener("popuphidden", onPopupHidden); + + aOnHidden && aOnHidden(); + + deferred.resolve(aPopup); + } + + let deferred = promise.defer(); + aPopup.addEventListener("popupshown", onPopupShown); + + info("wait for the context menu to open"); + let eventDetails = { type: "contextmenu", button: 2}; + EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails, + aButton.ownerDocument.defaultView); + return deferred.promise; +} + +/** + * Dump the output of all open Web Consoles - used only for debugging purposes. + */ +function dumpConsoles() +{ + if (gPendingOutputTest) { + console.log("dumpConsoles start"); + for (let [, hud] of HUDService.consoles) { + if (!hud.outputNode) { + console.debug("no output content for", hud.hudId); + continue; + } + + console.debug("output content for", hud.hudId); + for (let elem of hud.outputNode.childNodes) { + 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 = aMessage.textContent; + let repeats = aMessage.querySelector(".message-repeats"); + 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); +} + +let finishTest = Task.async(function* () { + dumpConsoles(); + + let browserConsole = HUDService.getBrowserConsole(); + if (browserConsole) { + if (browserConsole.jsterm) { + browserConsole.jsterm.clearOutput(true); + } + yield HUDService.toggleBrowserConsole(); + } + + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + finish(); +}); + +registerCleanupFunction(function*() { + gDevTools.testing = false; + + dumpConsoles(); + + if (HUDService.getBrowserConsole()) { + HUDService.toggleBrowserConsole(); + } + + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +waitForExplicitFinish(); + +/** + * Polls a given function waiting for it to become true. + * + * @param object aOptions + * Options object with the following properties: + * - validator + * A validator function that returns a boolean. This is called every few + * milliseconds to check if the result is true. When it is true, the + * promise is resolved and polling stops. If validator never returns + * true, then polling timeouts after several tries and the promise is + * rejected. + * - name + * Name of test. This is used to generate the success and failure + * messages. + * - timeout + * Timeout for validator function, in milliseconds. Default is 5000. + * @return object + * A Promise object that is resolved based on the validator function. + */ +function waitForSuccess(aOptions) +{ + let deferred = promise.defer(); + let start = Date.now(); + let timeout = aOptions.timeout || 5000; + let {validator} = aOptions; + + + function wait() + { + if ((Date.now() - start) > timeout) { + // Log the failure. + ok(false, "Timed out while waiting for: " + aOptions.name); + deferred.reject(null); + return; + } + + if (validator(aOptions)) { + ok(true, aOptions.name); + deferred.resolve(null); + } + else { + setTimeout(wait, 100); + } + } + + setTimeout(wait, 100); + + return deferred.promise; +} + +let openInspector = Task.async(function* (aTab = gBrowser.selectedTab) { + let target = TargetFactory.forTab(aTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + return toolbox.getCurrentPanel(); +}); + +/** + * Find variables or properties in a VariablesView instance. + * + * @param object aView + * The VariablesView instance. + * @param array aRules + * The array of rules you want to match. Each rule is an object with: + * - name (string|regexp): property name to match. + * - value (string|regexp): property value to match. + * - isIterator (boolean): check if the property is an iterator. + * - isGetter (boolean): check if the property is a getter. + * - isGenerator (boolean): check if the property is a generator. + * - dontMatch (boolean): make sure the rule doesn't match any property. + * @param object aOptions + * Options for matching: + * - webconsole: the WebConsole instance we work with. + * @return object + * A promise object that is resolved when all the rules complete + * matching. The resolved callback is given an array of all the rules + * you wanted to check. Each rule has a new property: |matchedProp| + * which holds a reference to the Property object instance from the + * VariablesView. If the rule did not match, then |matchedProp| is + * undefined. + */ +function findVariableViewProperties(aView, aRules, aOptions) +{ + // Initialize the search. + function init() + { + // Separate out the rules that require expanding properties throughout the + // view. + let expandRules = []; + let rules = aRules.filter((aRule) => { + if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { + expandRules.push(aRule); + return false; + } + return true; + }); + + // Search through the view those rules that do not require any properties to + // be expanded. Build the array of matchers, outstanding promises to be + // resolved. + let outstanding = []; + finder(rules, aView, outstanding); + + // Process the rules that need to expand properties. + let lastStep = processExpandRules.bind(null, expandRules); + + // Return the results - a promise resolved to hold the updated aRules array. + let returnResults = onAllRulesMatched.bind(null, aRules); + + return promise.all(outstanding).then(lastStep).then(returnResults); + } + + function onMatch(aProp, aRule, aMatched) + { + if (aMatched && !aRule.matchedProp) { + aRule.matchedProp = aProp; + } + } + + function finder(aRules, aVar, aPromises) + { + for (let [id, prop] of aVar) { + for (let rule of aRules) { + let matcher = matchVariablesViewProperty(prop, rule, aOptions); + aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + + function processExpandRules(aRules) + { + let rule = aRules.shift(); + if (!rule) { + return promise.resolve(null); + } + + let deferred = promise.defer(); + let expandOptions = { + rootVariable: aView, + expandTo: rule.name, + webconsole: aOptions.webconsole, + }; + + variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { + let name = rule.name; + let lastName = name.split(".").pop(); + rule.name = lastName; + + let matched = matchVariablesViewProperty(aProp, rule, aOptions); + return matched.then(onMatch.bind(null, aProp, rule)).then(function() { + rule.name = name; + }); + }, function onFailure() { + return promise.resolve(null); + }).then(processExpandRules.bind(null, aRules)).then(function() { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function onAllRulesMatched(aRules) + { + for (let rule of aRules) { + let matched = rule.matchedProp; + if (matched && !rule.dontMatch) { + ok(true, "rule " + rule.name + " matched for property " + matched.name); + } + else if (matched && rule.dontMatch) { + ok(false, "rule " + rule.name + " should not match property " + + matched.name); + } + else { + ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); + } + } + return aRules; + } + + return init(); +} + +/** + * Check if a given Property object from the variables view matches the given + * rule. + * + * @param object aProp + * The variable's view Property instance. + * @param object aRule + * Rules for matching the property. See findVariableViewProperties() for + * details. + * @param object aOptions + * Options for matching. See findVariableViewProperties(). + * @return object + * A promise that is resolved when all the checks complete. Resolution + * result is a boolean that tells your promise callback the match + * result: true or false. + */ +function matchVariablesViewProperty(aProp, aRule, aOptions) +{ + function resolve(aResult) { + return promise.resolve(aResult); + } + + if (aRule.name) { + let match = aRule.name instanceof RegExp ? + aRule.name.test(aProp.name) : + aProp.name == aRule.name; + if (!match) { + return resolve(false); + } + } + + if (aRule.value) { + let displayValue = aProp.displayValue; + if (aProp.displayValueClassName == "token-string") { + displayValue = displayValue.substring(1, displayValue.length - 1); + } + + let match = aRule.value instanceof RegExp ? + aRule.value.test(displayValue) : + displayValue == aRule.value; + if (!match) { + info("rule " + aRule.name + " did not match value, expected '" + + aRule.value + "', found '" + displayValue + "'"); + return resolve(false); + } + } + + if ("isGetter" in aRule) { + let isGetter = !!(aProp.getter && aProp.get("get")); + if (aRule.isGetter != isGetter) { + info("rule " + aRule.name + " getter test failed"); + return resolve(false); + } + } + + if ("isGenerator" in aRule) { + let isGenerator = aProp.displayValue == "Generator"; + if (aRule.isGenerator != isGenerator) { + info("rule " + aRule.name + " generator test failed"); + return resolve(false); + } + } + + let outstanding = []; + + if ("isIterator" in aRule) { + let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); + outstanding.push(isIterator.then((aResult) => { + if (aResult != aRule.isIterator) { + info("rule " + aRule.name + " iterator test failed"); + } + return aResult == aRule.isIterator; + })); + } + + outstanding.push(promise.resolve(true)); + + return promise.all(outstanding).then(function _onMatchDone(aResults) { + let ruleMatched = aResults.indexOf(false) == -1; + return resolve(ruleMatched); + }); +} + +/** + * Check if the given variables view property is an iterator. + * + * @param object aProp + * The Property instance you want to check. + * @param object aWebConsole + * The WebConsole instance to work with. + * @return object + * A promise that is resolved when the check completes. The resolved + * callback is given a boolean: true if the property is an iterator, or + * false otherwise. + */ +function isVariableViewPropertyIterator(aProp, aWebConsole) +{ + if (aProp.displayValue == "Iterator") { + return promise.resolve(true); + } + + let deferred = promise.defer(); + + variablesViewExpandTo({ + rootVariable: aProp, + expandTo: "__proto__.__iterator__", + webconsole: aWebConsole, + }).then(function onSuccess(aProp) { + deferred.resolve(true); + }, function onFailure() { + deferred.resolve(false); + }); + + return deferred.promise; +} + + +/** + * Recursively expand the variables view up to a given property. + * + * @param aOptions + * Options for view expansion: + * - rootVariable: start from the given scope/variable/property. + * - expandTo: string made up of property names you want to expand. + * For example: "body.firstChild.nextSibling" given |rootVariable: + * document|. + * - webconsole: a WebConsole instance. If this is not provided all + * property expand() calls will be considered sync. Things may fail! + * @return object + * A promise that is resolved only when the last property in |expandTo| + * is found, and rejected otherwise. Resolution reason is always the + * last property - |nextSibling| in the example above. Rejection is + * always the last property that was found. + */ +function variablesViewExpandTo(aOptions) +{ + let root = aOptions.rootVariable; + let expandTo = aOptions.expandTo.split("."); + let jsterm = (aOptions.webconsole || {}).jsterm; + let lastDeferred = promise.defer(); + + function fetch(aProp) + { + if (!aProp.onexpand) { + ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); + return promise.reject(aProp); + } + + let deferred = promise.defer(); + + if (aProp._fetched || !jsterm) { + executeSoon(function() { + deferred.resolve(aProp); + }); + } + else { + jsterm.once("variablesview-fetched", function _onFetchProp() { + executeSoon(() => deferred.resolve(aProp)); + }); + } + + aProp.expand(); + + return deferred.promise; + } + + function getNext(aProp) + { + let name = expandTo.shift(); + let newProp = aProp.get(name); + + if (expandTo.length > 0) { + ok(newProp, "found property " + name); + if (newProp) { + fetch(newProp).then(getNext, fetchError); + } + else { + lastDeferred.reject(aProp); + } + } + else { + if (newProp) { + lastDeferred.resolve(newProp); + } + else { + lastDeferred.reject(aProp); + } + } + } + + function fetchError(aProp) + { + lastDeferred.reject(aProp); + } + + if (!root._fetched) { + fetch(root).then(getNext, fetchError); + } + else { + getNext(root); + } + + return lastDeferred.promise; +} + + +/** + * Update the content of a property in the variables view. + * + * @param object aOptions + * Options for the property update: + * - property: the property you want to change. + * - field: string that tells what you want to change: + * - use "name" to change the property name, + * - or "value" to change the property value. + * - string: the new string to write into the field. + * - webconsole: reference to the Web Console instance we work with. + * @return object + * A Promise object that is resolved once the property is updated. + */ +let updateVariablesViewProperty = Task.async(function* (aOptions) { + let view = aOptions.property._variablesView; + view.window.focus(); + aOptions.property.focus(); + + switch (aOptions.field) { + case "name": + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window); + break; + case "value": + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); + break; + default: + throw new Error("options.field is incorrect"); + } + + let deferred = promise.defer(); + + executeSoon(() => { + EventUtils.synthesizeKey("A", { accelKey: true }, view.window); + + for (let c of aOptions.string) { + EventUtils.synthesizeKey(c, {}, view.window); + } + + if (aOptions.webconsole) { + aOptions.webconsole.jsterm.once("variablesview-fetched").then((varView) => { + deferred.resolve(varView); + }); + } + + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); + + if (!aOptions.webconsole) { + executeSoon(() => { + deferred.resolve(null); + }); + } + }); + + return deferred.promise; +}); + +/** + * Open the JavaScript debugger. + * + * @param object aOptions + * Options for opening the debugger: + * - tab: the tab you want to open the debugger for. + * @return object + * A promise that is resolved once the debugger opens, or rejected if + * the open fails. The resolution callback is given one argument, an + * object that holds the following properties: + * - target: the Target object for the Tab. + * - toolbox: the Toolbox instance. + * - panel: the jsdebugger panel instance. + * - panelWin: the window object of the panel iframe. + */ +function openDebugger(aOptions = {}) +{ + if (!aOptions.tab) { + aOptions.tab = gBrowser.selectedTab; + } + + let deferred = promise.defer(); + + let target = TargetFactory.forTab(aOptions.tab); + let toolbox = gDevTools.getToolbox(target); + let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger"); + + gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { + let panel = aToolbox.getCurrentPanel(); + let panelWin = panel.panelWin; + + panel._view.Variables.lazyEmpty = false; + + let resolveObject = { + target: target, + toolbox: aToolbox, + panel: panel, + panelWin: panelWin, + }; + + if (dbgPanelAlreadyOpen) { + deferred.resolve(resolveObject); + } + else { + panelWin.once(panelWin.EVENTS.SOURCES_ADDED, () => { + deferred.resolve(resolveObject); + }); + } + }, function onFailure(aReason) { + console.debug("failed to open the toolbox for 'jsdebugger'", aReason); + deferred.reject(aReason); + }); + + return deferred.promise; +} + +/** + * Wait for messages in the Web Console output. + * + * @param object aOptions + * Options for what you want to wait for: + * - webconsole: the webconsole instance you work with. + * - matchCondition: "any" or "all". Default: "all". The promise + * returned by this function resolves when all of the messages are + * matched, if the |matchCondition| is "all". If you set the condition to + * "any" then the promise is resolved by any message rule that matches, + * irrespective of order - waiting for messages stops whenever any rule + * matches. + * - messages: an array of objects that tells which messages to wait for. + * Properties: + * - text: string or RegExp to match the textContent of each new + * message. + * - noText: string or RegExp that must not match in the message + * textContent. + * - repeats: the number of message repeats, as displayed by the Web + * Console. + * - category: match message category. See CATEGORY_* constants at + * the top of this file. + * - severity: match message severity. See SEVERITY_* constants at + * the top of this file. + * - count: how many unique web console messages should be matched by + * this rule. + * - consoleTrace: boolean, set to |true| to match a console.trace() + * message. Optionally this can be an object of the form + * { file, fn, line } that can match the specified file, function + * and/or line number in the trace message. + * - consoleTime: string that matches a console.time() timer name. + * Provide this if you want to match a console.time() message. + * - consoleTimeEnd: same as above, but for console.timeEnd(). + * - consoleDir: boolean, set to |true| to match a console.dir() + * message. + * - consoleGroup: boolean, set to |true| to match a console.group() + * message. + * - consoleTable: boolean, set to |true| to match a console.table() + * message. + * - longString: boolean, set to |true} to match long strings in the + * message. + * - collapsible: boolean, set to |true| to match messages that can + * be collapsed/expanded. + * - 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 of the shape { url, line }. This is used to + * match the source URL and line number of the error message or + * console API call. + * - stacktrace: array of objects of the form { file, fn, line } that + * can match frames in the stacktrace associated with the message. + * - groupDepth: number used to check the depth of the message in + * a group. + * - url: URL to match for network requests. + * @return object + * A promise object is returned once the messages you want are found. + * The promise is resolved with the array of rule objects you give in + * the |messages| property. Each objects is the same as provided, with + * additional properties: + * - matched: a Set of web console messages that matched the rule. + * - clickableElements: a list of inspectable objects. This is available + * if any of the following properties are present in the rule: + * |consoleTrace| or |objects|. + * - longStrings: a list of long string ellipsis elements you can click + * in the message element, to expand a long string. This is available + * only if |longString| is present in the matching rule. + */ +function waitForMessages(aOptions) +{ + info("Waiting for messages..."); + + gPendingOutputTest++; + let webconsole = aOptions.webconsole; + let rules = WebConsoleUtils.cloneObject(aOptions.messages, true); + let rulesMatched = 0; + let listenerAdded = false; + let deferred = promise.defer(); + aOptions.matchCondition = aOptions.matchCondition || "all"; + + function checkText(aRule, aText) + { + let result = false; + if (Array.isArray(aRule)) { + result = aRule.every((s) => checkText(s, aText)); + } + else if (typeof aRule == "string") { + result = aText.indexOf(aRule) > -1; + } + else if (aRule instanceof RegExp) { + result = aRule.test(aText); + } + else { + result = aRule == aText; + } + return result; + } + + function checkConsoleTable(aRule, aElement) + { + let elemText = aElement.textContent; + let table = aRule.consoleTable; + + if (!checkText("console.table():", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + aRule.type = Messages.ConsoleTable; + + return true; + } + + function checkConsoleTrace(aRule, aElement) + { + let elemText = aElement.textContent; + let trace = aRule.consoleTrace; + + if (!checkText("console.trace():", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + aRule.type = Messages.ConsoleTrace; + + if (!aRule.stacktrace && typeof trace == "object" && trace !== true) { + if (Array.isArray(trace)) { + aRule.stacktrace = trace; + } else { + aRule.stacktrace = [trace]; + } + } + + return true; + } + + function checkConsoleTime(aRule, aElement) + { + let elemText = aElement.textContent; + 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 = aElement.textContent; + let time = aRule.consoleTimeEnd; + let regex = new RegExp(time + ": -?\\d+([,.]\\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("inlined-variables-view")) { + return false; + } + + let elemText = aElement.textContent; + 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 checkConsoleGroup(aRule, aElement) + { + if (!isNaN(parseInt(aRule.consoleGroup))) { + aRule.groupDepth = aRule.consoleGroup; + } + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkSource(aRule, aElement) + { + let location = aElement.querySelector(".message-location"); + if (!location) { + return false; + } + + if (!checkText(aRule.source.url, location.getAttribute("title"))) { + return false; + } + + if ("line" in aRule.source && location.sourceLine != aRule.source.line) { + return false; + } + + return true; + } + + function checkCollapsible(aRule, aElement) + { + let msg = aElement._messageObject; + if (!msg || !!msg.collapsible != aRule.collapsible) { + return false; + } + + return true; + } + + function checkStacktrace(aRule, aElement) + { + let stack = aRule.stacktrace; + let frames = aElement.querySelectorAll(".stacktrace > li"); + if (!frames.length) { + return false; + } + + for (let i = 0; i < stack.length; i++) { + let frame = frames[i]; + let expected = stack[i]; + if (!frame) { + ok(false, "expected frame #" + i + " but didnt find it"); + return false; + } + + if (expected.file) { + let file = frame.querySelector(".message-location").title; + if (!checkText(expected.file, file)) { + ok(false, "frame #" + i + " does not match file name: " + + expected.file); + displayErrorContext(aRule, aElement); + return false; + } + } + + if (expected.fn) { + let fn = frame.querySelector(".function").textContent; + if (!checkText(expected.fn, fn)) { + ok(false, "frame #" + i + " does not match the function name: " + + expected.fn); + displayErrorContext(aRule, aElement); + return false; + } + } + + if (expected.line) { + let line = frame.querySelector(".message-location").sourceLine; + if (!checkText(expected.line, line)) { + ok(false, "frame #" + i + " does not match the line number: " + + expected.line); + displayErrorContext(aRule, aElement); + return false; + } + } + } + + return true; + } + + function hasXhrLabel(aElement) { + let xhr = aElement.querySelector('.xhr'); + if (!xhr) { + return false; + } + return true; + } + + function checkMessage(aRule, aElement) + { + let elemText = aElement.textContent; + + if (aRule.text && !checkText(aRule.text, elemText)) { + return false; + } + + if (aRule.noText && checkText(aRule.noText, elemText)) { + return false; + } + + if (aRule.consoleTable && !checkConsoleTable(aRule, aElement)) { + return false; + } + + if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) { + 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; + } + + if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) { + return false; + } + + if (aRule.source && !checkSource(aRule, aElement)) { + return false; + } + + if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) { + return false; + } + + if (aRule.isXhr && !hasXhrLabel(aElement)) { + return false; + } + + if (!aRule.isXhr && hasXhrLabel(aElement)) { + return false; + } + + let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || + aRule.consoleTimeEnd); + + // The rule tries to match the newer types of messages, based on their + // object constructor. + if (aRule.type) { + if (!aElement._messageObject || + !(aElement._messageObject instanceof aRule.type)) { + if (partialMatch) { + ok(false, "message type for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + partialMatch = true; + } + + if ("category" in aRule && aElement.category != aRule.category) { + if (partialMatch) { + is(aElement.category, aRule.category, + "message category for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if ("severity" in aRule && aElement.severity != aRule.severity) { + if (partialMatch) { + is(aElement.severity, aRule.severity, + "message severity for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if (aRule.text) { + partialMatch = true; + } + + if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) { + if (partialMatch) { + ok(false, "failed to match stacktrace for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if (aRule.category == CATEGORY_NETWORK && "url" in aRule && + !checkText(aRule.url, aElement.url)) { + return false; + } + + if ("repeats" in aRule) { + let repeats = aElement.querySelector(".message-repeats"); + if (!repeats || repeats.getAttribute("value") != aRule.repeats) { + return false; + } + } + + if ("groupDepth" in aRule) { + let indentNode = aElement.querySelector(".indent"); + let indent = (GROUP_INDENT * aRule.groupDepth) + "px"; + if (!indentNode || indentNode.style.width != indent) { + is(indentNode.style.width, indent, + "group depth check failed for message rule: " + displayRule(aRule)); + 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(".message-body a"); + 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; + if (!aRule.matched) { + aRule.matched = new Set(); + } + aRule.matched.add(aElement); + + return aRule.matched.size == count; + } + + function onMessagesAdded(aEvent, aNewMessages) + { + for (let msg of aNewMessages) { + let elem = msg.node; + let location = elem.querySelector(".message-location"); + if (location) { + let url = location.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; + } + + let matched = checkMessage(rule, elem); + if (matched) { + rule._ruleMatched = true; + rulesMatched++; + ok(1, "matched rule: " + displayRule(rule)); + if (maybeDone()) { + return; + } + } + } + } + } + + function allRulesMatched() + { + return aOptions.matchCondition == "all" && rulesMatched == rules.length || + aOptions.matchCondition == "any" && rulesMatched > 0; + } + + function maybeDone() + { + if (allRulesMatched()) { + if (listenerAdded) { + webconsole.ui.off("new-messages", onMessagesAdded); + } + gPendingOutputTest--; + deferred.resolve(rules); + return true; + } + return false; + } + + function testCleanup() { + if (allRulesMatched()) { + return; + } + + if (webconsole.ui) { + webconsole.ui.off("new-messages", onMessagesAdded); + } + + for (let rule of rules) { + if (!rule._ruleMatched) { + ok(false, "failed to match rule: " + displayRule(rule)); + } + } + } + + function displayRule(aRule) + { + 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(() => { + + let messages = []; + for (let elem of webconsole.outputNode.childNodes) { + messages.push({ + node: elem, + update: false, + }); + } + + onMessagesAdded("new-messages", messages); + + if (!allRulesMatched()) { + listenerAdded = true; + registerCleanupFunction(testCleanup); + webconsole.ui.on("new-messages", onMessagesAdded); + } + }); + + return deferred.promise; +} + +function whenDelayedStartupFinished(aWindow, aCallback) +{ + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); +} + +/** + * Check the web console output for the given inputs. Each input is checked for + * the expected JS eval result, the result of calling print(), the result of + * console.log(). The JS eval result is also checked if it opens the variables + * view on click. + * + * @param object hud + * The web console instance to work with. + * @param array inputTests + * An array of input tests. An input test element is an object. Each + * object has the following properties: + * - input: string, JS input value to execute. + * + * - output: string|RegExp, expected JS eval result. + * + * - inspectable: boolean, when true, the test runner expects the JS eval + * result is an object that can be clicked for inspection. + * + * - noClick: boolean, when true, the test runner does not click the JS + * eval result. Some objects, like |window|, have a lot of properties and + * opening vview for them is very slow (they can cause timeouts in debug + * builds). + * + * - printOutput: string|RegExp, optional, expected output for + * |print(input)|. If this is not provided, printOutput = output. + * + * - variablesViewLabel: string|RegExp, optional, the expected variables + * view label when the object is inspected. If this is not provided, then + * |output| is used. + * + * - inspectorIcon: boolean, when true, the test runner expects the + * result widget to contain an inspectorIcon element (className + * open-inspector). + * + * - expectedTab: string, optional, the full URL of the new tab which must + * open. If this is not provided, any new tabs that open will cause a test + * failure. + */ +function checkOutputForInputs(hud, inputTests) +{ + let container = gBrowser.tabContainer; + + function* runner() + { + for (let [i, entry] of inputTests.entries()) { + info("checkInput(" + i + "): " + entry.input); + yield checkInput(entry); + } + container = null; + } + + function* checkInput(entry) + { + yield checkConsoleLog(entry); + yield checkPrintOutput(entry); + yield checkJSEval(entry); + } + + function* checkConsoleLog(entry) + { + info("Logging: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute("console.log(" + entry.input + ")"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log() output: " + entry.output, + text: entry.output, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + if (typeof entry.inspectorIcon == "boolean") { + let msg = [...result.matched][0]; + yield checkLinkToInspector(entry, msg); + } + } + + function checkPrintOutput(entry) + { + info("Printing: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute("print(" + entry.input + ")"); + + let printOutput = entry.printOutput || entry.output; + + return waitForMessages({ + webconsole: hud, + messages: [{ + name: "print() output: " + printOutput, + text: printOutput, + category: CATEGORY_OUTPUT, + }], + }); + } + + function* checkJSEval(entry) + { + info("Evaluating: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute(entry.input); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "JS eval output: " + entry.output, + text: entry.output, + category: CATEGORY_OUTPUT, + }], + }); + + let msg = [...result.matched][0]; + if (!entry.noClick) { + yield checkObjectClick(entry, msg); + } + if (typeof entry.inspectorIcon == "boolean") { + yield checkLinkToInspector(entry, msg); + } + } + + function* checkObjectClick(entry, msg) + { + info("Clicking: " + entry.input); + let body = msg.querySelector(".message-body a") || + msg.querySelector(".message-body"); + ok(body, "the message body"); + + let deferredVariablesView = promise.defer(); + entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferredVariablesView); + hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen); + + let deferredTab = promise.defer(); + entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab); + container.addEventListener("TabOpen", entry._onTabOpen, true); + + body.scrollIntoView(); + EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow); + + if (entry.inspectable) { + info("message body tagName '" + body.tagName + "' className '" + body.className + "'"); + yield deferredVariablesView.promise; + } else { + hud.jsterm.off("variablesview-open", entry._onVariablesView); + entry._onVariablesView = null; + } + + if (entry.expectedTab) { + yield deferredTab.promise; + } else { + container.removeEventListener("TabOpen", entry._onTabOpen, true); + entry._onTabOpen = null; + } + + yield promise.resolve(null); + } + + function checkLinkToInspector(entry, msg) + { + info("Checking Inspector Link: " + entry.input); + let elementNodeWidget = [...msg._messageObject.widgets][0]; + if (!elementNodeWidget) { + ok(!entry.inspectorIcon, "The message has no ElementNode widget"); + return; + } + + return elementNodeWidget.linkToInspector().then(() => { + // linkToInspector resolved, check for the .open-inspector element + if (entry.inspectorIcon) { + ok(msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget is linked to the inspector"); + } else { + ok(!msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget isn't linked to the inspector"); + } + }, () => { + // linkToInspector promise rejected, node not linked to inspector + ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector"); + }); + } + + function onVariablesViewOpen(entry, {resolve, reject}, event, view, options) + { + info("Variables view opened: " + entry.input); + let label = entry.variablesViewLabel || entry.output; + if (typeof label == "string" && options.label != label) { + return; + } + if (label instanceof RegExp && !label.test(options.label)) { + return; + } + + hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen); + entry._onVariablesViewOpen = null; + ok(entry.inspectable, "variables view was shown"); + + resolve(null); + } + + function onTabOpen(entry, {resolve, reject}, event) + { + container.removeEventListener("TabOpen", entry._onTabOpen, true); + entry._onTabOpen = null; + + let tab = event.target; + let browser = gBrowser.getBrowserForTab(tab); + loadBrowser(browser).then(() => { + let uri = content.location.href; + ok(entry.expectedTab && entry.expectedTab == uri, + "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'"); + return closeTab(tab); + }).then(resolve, reject); + } + + return Task.spawn(runner); +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture=false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + }, useCapture); + break; + } + } + + return deferred.promise; +} + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item && item.value; +} + +/** + * Verify that clicking on a link from a popup notification message tries to + * open the expected URL. + */ +function simulateMessageLinkClick(element, expectedLink) { + let deferred = promise.defer(); + + // Invoke the click event and check if a new tab would + // open to the correct page. + let oldOpenUILinkIn = window.openUILinkIn; + window.openUILinkIn = function(link) { + if (link == expectedLink) { + ok(true, "Clicking the message link opens the desired page"); + window.openUILinkIn = oldOpenUILinkIn; + deferred.resolve(); + } + }; + + let event = new MouseEvent("click", { + detail: 1, + button: 0, + bubbles: true, + cancelable: true + }); + element.dispatchEvent(event); + + return deferred.promise; +} diff --git a/browser/devtools/webconsole/test/test-autocomplete-in-stackframe.html b/browser/devtools/webconsole/test/test-autocomplete-in-stackframe.html new file mode 100644 index 000000000..ba5212de3 --- /dev/null +++ b/browser/devtools/webconsole/test/test-autocomplete-in-stackframe.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 842682 - use the debugger API for web console autocomplete</title> + <script> + var foo1 = "globalFoo"; + + var foo1Obj = { + prop1: "111", + prop2: { + prop21: "212121" + } + }; + + function firstCall() + { + var foo2 = "fooFirstCall"; + + var foo2Obj = { + prop1: { + prop11: "111111" + } + }; + + secondCall(); + } + + function secondCall() + { + var foo3 = "fooSecondCall"; + + var foo3Obj = { + prop1: { + prop11: "313131" + } + }; + + debugger; + } + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-585956-console-trace.html b/browser/devtools/webconsole/test/test-bug-585956-console-trace.html new file mode 100644 index 000000000..e658ba633 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-585956-console-trace.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html lang="en"> + <head><meta charset="utf-8"> + <title>Web Console test for bug 585956 - console.trace()</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<script type="application/javascript"> +window.foobar585956c = function(a) { + console.trace(); + return a+"c"; +}; + +function foobar585956b(a) { + return foobar585956c(a+"b"); +} + +function foobar585956a(omg) { + return foobar585956b(omg + "a"); +} + +foobar585956a("omg"); +</script> + </head> + <body> + <p>Web Console test for bug 585956 - console.trace().</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html new file mode 100644 index 000000000..ebf9c515f --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>WebConsole test: iframe associated to the wrong HUD</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>WebConsole test: iframe associated to the wrong HUD.</p> + <p>This is the iframe!</p> + </body> + </html> diff --git a/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html new file mode 100644 index 000000000..5b12278d1 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>WebConsole test: iframe associated to the wrong HUD</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>WebConsole test: iframe associated to the wrong HUD.</p> + <iframe + src="http://example.com/browser/browser/devtools/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"></iframe> + </body> + </html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-canvas-css.html b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.html new file mode 100644 index 000000000..3c9cf03a5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Parser (with + Canvas)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" + src="test-bug-595934-canvas-css.js"></script> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Parser" (with + Canvas).</p> + <p><canvas width="200" height="200">Canvas support is required!</canvas></p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js new file mode 100644 index 000000000..cc364d6a3 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-canvas-css.js @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +window.addEventListener("DOMContentLoaded", function() { + var canvas = document.querySelector("canvas"); + var context = canvas.getContext("2d"); + context.strokeStyle = "foobarCanvasCssParser"; +}, false); diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-loader.css b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css new file mode 100644 index 000000000..b4224430f --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0f0; + font-weight: bold; +} + diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^ new file mode 100644 index 000000000..e7be84a71 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.css^headers^ @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-loader.html b/browser/devtools/webconsole/test/test-bug-595934-css-loader.html new file mode 100644 index 000000000..6bb0d54c5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-css-loader.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Loader</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" href="test-bug-595934-css-loader.css"> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Loader".</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-parser.css b/browser/devtools/webconsole/test/test-bug-595934-css-parser.css new file mode 100644 index 000000000..f6db82398 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-css-parser.css @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +p { + color: #0f0; + foobarCssParser: failure; +} + diff --git a/browser/devtools/webconsole/test/test-bug-595934-css-parser.html b/browser/devtools/webconsole/test/test-bug-595934-css-parser.html new file mode 100644 index 000000000..a4ea74ba3 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-css-parser.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: CSS Parser</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" type="text/css" + href="test-bug-595934-css-parser.css"> + </head> + <body> + <p>Web Console test for bug 595934 - category "CSS Parser".</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html new file mode 100644 index 000000000..a70f9011b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: DOM. + (empty getElementById())</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" + src="test-bug-595934-empty-getelementbyid.js"></script> + </head> + <body> + <p>Web Console test for bug 595934 - category "DOM" + (empty getElementById()).</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js new file mode 100644 index 000000000..dd94d716d --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-empty-getelementbyid.js @@ -0,0 +1,8 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +window.addEventListener("load", function() { + document.getElementById(""); +}, false); diff --git a/browser/devtools/webconsole/test/test-bug-595934-html.html b/browser/devtools/webconsole/test/test-bug-595934-html.html new file mode 100644 index 000000000..fe35afef6 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-html.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: HTML</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "HTML".</p> + <form action="?" enctype="multipart/form-data"> + <p><label>Input <input type="text" value="test value"></label></p> + </form> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-595934-image.html b/browser/devtools/webconsole/test/test-bug-595934-image.html new file mode 100644 index 000000000..312ecd49f --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-image.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: Image</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category Image.</p> + <p><img src="test-bug-595934-image.jpg" alt="corrupted image"></p> + </body> +</html> + + diff --git a/browser/devtools/webconsole/test/test-bug-595934-image.jpg b/browser/devtools/webconsole/test/test-bug-595934-image.jpg Binary files differnew file mode 100644 index 000000000..947e5f11b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-image.jpg diff --git a/browser/devtools/webconsole/test/test-bug-595934-imagemap.html b/browser/devtools/webconsole/test/test-bug-595934-imagemap.html new file mode 100644 index 000000000..007c3c01b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-imagemap.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: ImageMap</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "ImageMap".</p> + <p><img src="test-image.png" usemap="#testMap" alt="Test image"></p> + <map name="testMap"> + <area shape="rect" coords="0,0,10,10,5" href="#" alt="Test area" /> + </map> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html new file mode 100644 index 000000000..2fd8beac5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: malformed-xml. + (external file)</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"><!-- + var req = new XMLHttpRequest(); + req.open("GET", "test-bug-595934-malformedxml-external.xml", true); + req.send(null); + // --></script> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml" + (external file).</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml new file mode 100644 index 000000000..4812786f1 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-malformedxml-external.xml @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml".</p> + </body> diff --git a/browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml b/browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml new file mode 100644 index 000000000..62689c567 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-malformedxml.xhtml @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>Web Console test for bug 595934 - category: malformed-xml</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "malformed-xml".</p> + </body> diff --git a/browser/devtools/webconsole/test/test-bug-595934-svg.xhtml b/browser/devtools/webconsole/test/test-bug-595934-svg.xhtml new file mode 100644 index 000000000..572382c64 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-svg.xhtml @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>Web Console test for bug 595934 - category: SVG</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - category "SVG".</p> + <svg version="1.1" width="120" height="fooBarSVG" + xmlns="http://www.w3.org/2000/svg"> + <ellipse fill="#0f0" stroke="#000" cx="50%" + cy="50%" rx="50%" ry="50%" /> + </svg> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-595934-workers.html b/browser/devtools/webconsole/test/test-bug-595934-workers.html new file mode 100644 index 000000000..baf5a6215 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-workers.html @@ -0,0 +1,18 @@ +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 595934 - category: DOM Worker + javascript</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p id="foobar">Web Console test for bug 595934 - category "DOM Worker + javascript".</p> + <script type="text/javascript"> + var myWorker = new Worker("test-bug-595934-workers.js"); + myWorker.postMessage("hello world"); + </script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-595934-workers.js b/browser/devtools/webconsole/test/test-bug-595934-workers.js new file mode 100644 index 000000000..4e93c967b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-595934-workers.js @@ -0,0 +1,9 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function() { + fooBarWorker(); +} + diff --git a/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html new file mode 100644 index 000000000..25bdeecc5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> +<!-- + ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + --> + <title>Test for bug 597136: external script errors</title> + </head> + <body> + <h1>Test for bug 597136: external script errors</h1> + <p><button onclick="f()">Click me</button</p> + + <script type="text/javascript" + src="test-bug-597136-external-script-errors.js"></script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js new file mode 100644 index 000000000..87c0aff8e --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-597136-external-script-errors.js @@ -0,0 +1,14 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributor(s): + * Patrick Walton <pcwalton@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +function f() { + bogus.g(); +} + diff --git a/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html b/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html new file mode 100644 index 000000000..68e19e677 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Bug 597756: test error logging after tab close and reopen</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <h1>Bug 597756: test error logging after tab close and reopen.</h1> + + <script type="text/javascript"><!-- + fooBug597756_error.bar(); + // --></script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs b/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs new file mode 100644 index 000000000..2e78d6b7b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs @@ -0,0 +1,25 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) +{ + var Etag = '"4c881ab-b03-435f0a0f9ef00"'; + var IfNoneMatch = request.hasHeader("If-None-Match") + ? request.getHeader("If-None-Match") + : ""; + + var page = "<!DOCTYPE html><html><body><p>hello world!</p></body></html>"; + + response.setHeader("Etag", Etag, false); + + if (IfNoneMatch == Etag) { + response.setStatusLine(request.httpVersion, "304", "Not Modified"); + } + else { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); + } +} diff --git a/browser/devtools/webconsole/test/test-bug-600183-charset.html b/browser/devtools/webconsole/test/test-bug-600183-charset.html new file mode 100644 index 000000000..040490a6b --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-600183-charset.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="gb2312"> + <title>Console HTTP test page (chinese)</title> + </head> + <body> + <p>µÄÎʺò!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^ b/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^ new file mode 100644 index 000000000..9f3e2302f --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-600183-charset.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=gb2312 diff --git a/browser/devtools/webconsole/test/test-bug-601177-log-levels.html b/browser/devtools/webconsole/test/test-bug-601177-log-levels.html new file mode 100644 index 000000000..a59213907 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-601177-log-levels.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 601177: log levels</title> + <script src="test-bug-601177-log-levels.js" type="text/javascript"></script> + <script type="text/javascript"><!-- + window.undefinedPropertyBug601177; + // --></script> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <h1>Web Console test for bug 601177: log levels</h1> + <img src="test-image.png?bug601177"> + <img src="foobar-known-to-fail.png?bug601177"> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-601177-log-levels.js b/browser/devtools/webconsole/test/test-bug-601177-log-levels.js new file mode 100644 index 000000000..ea37f533d --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-601177-log-levels.js @@ -0,0 +1,8 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +foobarBug601177strictError = "strict error"; + +window.foobarBug601177exception(); diff --git a/browser/devtools/webconsole/test/test-bug-603750-websocket.html b/browser/devtools/webconsole/test/test-bug-603750-websocket.html new file mode 100644 index 000000000..f0097dd77 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-603750-websocket.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 603750 - Web Socket errors</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 595934 - Web Socket errors.</p> + <iframe src="data:text/html;charset=utf-8,hello world!"></iframe> + <script type="text/javascript" src="test-bug-603750-websocket.js"></script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-603750-websocket.js b/browser/devtools/webconsole/test/test-bug-603750-websocket.js new file mode 100644 index 000000000..3746424cc --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-603750-websocket.js @@ -0,0 +1,18 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +window.addEventListener("load", function () { + var ws1 = new WebSocket("ws://0.0.0.0:81"); + ws1.onopen = function() { + ws1.send("test 1"); + ws1.close(); + }; + + var ws2 = new window.frames[0].WebSocket("ws://0.0.0.0:82"); + ws2.onopen = function() { + ws2.send("test 2"); + ws2.close(); + }; +}, false); diff --git a/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html new file mode 100644 index 000000000..451eba21e --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 609872 - iframe child</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>test for bug 609872 - iframe child</p> + <script>window.foobarBug609872 = 'child!';</script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html new file mode 100644 index 000000000..fdb636b97 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 609872 - iframe parent</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>test for bug 609872 - iframe parent</p> + <script>window.foobarBug609872 = 'parent!';</script> + <iframe src="test-bug-609872-cd-iframe-child.html"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html b/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html new file mode 100644 index 000000000..edf40e80e --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-613013-console-api-iframe.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 613013</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>test for bug 613013</p> + <script type="text/javascript"><!-- + (function () { + var iframe = document.createElement('iframe'); + iframe.src = 'data:text/html;charset=utf-8,little iframe'; + document.body.appendChild(iframe); + + console.log("foobarBug613013"); + })(); + // --></script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html b/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html new file mode 100644 index 000000000..ac755e1b9 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 618078 - exception in async network request + callback</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + var req = new XMLHttpRequest(); + req.open('GET', 'http://example.com', true); + req.onreadystatechange = function() { + if (req.readyState == 4) { + bug618078exception(); + } + }; + req.send(null); + </script> + </head> + <body> + <p>Web Console test for bug 618078 - exception in async network request + callback.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html b/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html new file mode 100644 index 000000000..09c986703 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 621644</title> + <script> + function $(elem) { + return elem.innerHTML; + } + function $$(doc) { + return doc.title; + } + </script> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <h1>Web Console test for bug 621644</h1> + <p>hello world!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs b/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs new file mode 100644 index 000000000..f92e0fe65 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs @@ -0,0 +1,16 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) +{ + var page = "<!DOCTYPE html><html><body><p>hello world! bug 630733</p></body></html>"; + + response.setStatusLine(request.httpVersion, "301", "Moved Permanently"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.setHeader("x-foobar-bug630733", "bazbaz", false); + response.setHeader("Location", "/redirect-from-bug-630733", false); + response.write(page); +} diff --git a/browser/devtools/webconsole/test/test-bug-632275-getters.html b/browser/devtools/webconsole/test/test-bug-632275-getters.html new file mode 100644 index 000000000..349c301f3 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-632275-getters.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 632275 - getters</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<script type="application/javascript;version=1.8"> + document.foobar = { + _val: 5, + get val() { return ++this._val; } + }; +</script> + + </head> + <body> + <p>Web Console test for bug 632275 - getters.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html b/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html new file mode 100644 index 000000000..a6080c642 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-632347-iterators-generators.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 632347 - iterators and generators</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<script type="application/javascript;version=1.8"> +(function(){ +function genFunc() { + var a = 5; + while (a < 10) { + yield a++; + } +} + +window._container = {}; + +_container.gen1 = genFunc(); +_container.gen1.next(); + +var obj = { foo: "bar", baz: "baaz", hay: "stack" }; +_container.iter1 = Iterator(obj); + +function Range(low, high) { + this.low = low; + this.high = high; +} + +function RangeIterator(range) { + this.range = range; + this.current = this.range.low; +} + +RangeIterator.prototype.next = function() { + if (this.current > this.range.high) { + throw StopIteration; + } else { + return this.current++; + } +} + +Range.prototype.__iterator__ = function() { + return new RangeIterator(this); +} + +_container.iter2 = new Range(3, 15); + +_container.gen2 = (i * 2 for (i in _container.iter2)); +})(); +</script> + </head> + <body> + <p>Web Console test for bug 632347 - iterators and generators.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-644419-log-limits.html b/browser/devtools/webconsole/test/test-bug-644419-log-limits.html new file mode 100644 index 000000000..21d99ba14 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-644419-log-limits.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset="utf-8"> + <title>Test for bug 644419: console log limits</title> + </head> + <body> + <h1>Test for bug 644419: Console should have user-settable log limits for + each message category</h1> + + <script type="text/javascript"> + function foo() { + bar.baz(); + } + foo(); + </script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-646025-console-file-location.html b/browser/devtools/webconsole/test/test-bug-646025-console-file-location.html new file mode 100644 index 000000000..7c80f1446 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-646025-console-file-location.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console file location test</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script src="test-file-location.js"></script> + </head> + <body> + <h1>Web Console File Location Test Page</h1> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-658368-time-methods.html b/browser/devtools/webconsole/test/test-bug-658368-time-methods.html new file mode 100644 index 000000000..cc50b6313 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-658368-time-methods.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset="utf-8"> + <title>Test for bug 658368: Expand console object with time and timeEnd + methods</title> + </head> + <body> + <h1>Test for bug 658368: Expand console object with time and timeEnd + methods</h1> + + <script type="text/javascript"> + function foo() { + console.timeEnd("aTimer"); + } + console.time("aTimer"); + foo(); + console.time("bTimer"); + </script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html b/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html new file mode 100644 index 000000000..db83274f0 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf8"> + <title>Mixed Content test - http on https</title> + <script src="testscript.js"></script> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe src = "http://example.com"></iframe> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html new file mode 100644 index 000000000..ccb363ed9 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>I am sandboxed and want to escape.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html new file mode 100644 index 000000000..273e1a4e5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe +src="http://www.example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html new file mode 100644 index 000000000..21f5dd672 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe +src="http://www.example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html new file mode 100644 index 000000000..233a6cb70 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html new file mode 100644 index 000000000..da0d58819 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, no allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html new file mode 100644 index 000000000..f33f0a6dc --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (no allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe src="test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-same-origin"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html new file mode 100644 index 000000000..1d8f5ac47 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe +src="http://www.example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html new file mode 100644 index 000000000..7c749b8c7 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (allow-scripts, allow-same-origin, nested)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe +src="http://www.example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html new file mode 100644 index 000000000..7aad0b2c5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 752559 - print warning to error console when iframe sandbox + is being used ineffectively (nested, allow-scripts, allow-same-origin)</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <iframe +src="http://www.example.com/browser/browser/devtools/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html b/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html new file mode 100644 index 000000000..d7bcd45d6 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html @@ -0,0 +1,28 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 762593 - Add warning/error Message to Web Console when the + page includes Insecure Password fields</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + + <!-- This test tests the scenario where a javascript adds password fields to + an about:blank iframe inside an insecure web page. It ensures that + insecure password fields like those are detected and a warning is sent to + the web console. --> + </head> + <body> + <p>This insecure page is served with an about:blank iframe. A script then adds a + password field to it.</p> + <iframe id = "myiframe" width = "300" height="300" > + </iframe> + <script> + var doc = window.document; + var myIframe = doc.getElementById("myiframe"); + myIframe.contentDocument.open(); + myIframe.contentDocument.write("<form><input type = 'password' name='pwd' value='test'> </form>"); + myIframe.contentDocument.close(); + </script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html b/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html new file mode 100644 index 000000000..a4d0d7843 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 762593 - Add warning/error Message to Web Console when the + page includes Insecure Password fields</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>This page is served with an iframe with insecure password field.</p> + <iframe src + ="http://example.com/browser/browser/devtools/webconsole/test/test-iframe-762593-insecure-frame.html"> + </iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-766001-console-log.js b/browser/devtools/webconsole/test/test-bug-766001-console-log.js new file mode 100644 index 000000000..8f6a3bb9c --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-766001-console-log.js @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function onLoad123() { + console.log("Blah Blah"); +} + +window.addEventListener("load", onLoad123, false); diff --git a/browser/devtools/webconsole/test/test-bug-766001-js-console-links.html b/browser/devtools/webconsole/test/test-bug-766001-js-console-links.html new file mode 100644 index 000000000..6a6ac6008 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-766001-js-console-links.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 766001 : Open JS/Console call Links in Debugger</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-bug-766001-js-errors.js"></script> + <script type="text/javascript" src="test-bug-766001-console-log.js"></script> + </head> + <body> + <p>Web Console test for bug 766001 : Open JS/Console call Links in Debugger.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-766001-js-errors.js b/browser/devtools/webconsole/test/test-bug-766001-js-errors.js new file mode 100644 index 000000000..932204395 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-766001-js-errors.js @@ -0,0 +1,7 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +window.addEventListener("load", function() { + document.bar(); +}, false); diff --git a/browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css b/browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css new file mode 100644 index 000000000..ad7fd1999 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-782653-css-errors-1.css @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0f0; + font-weight: green; +} + diff --git a/browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css b/browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css new file mode 100644 index 000000000..91b14137a --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-782653-css-errors-2.css @@ -0,0 +1,10 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +body { + color: #0fl; + font-weight: bold; +} + diff --git a/browser/devtools/webconsole/test/test-bug-782653-css-errors.html b/browser/devtools/webconsole/test/test-bug-782653-css-errors.html new file mode 100644 index 000000000..7ca11fc34 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-782653-css-errors.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 782653 : Open CSS Links in Style Editor</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <link rel="stylesheet" href="test-bug-782653-css-errors-1.css"> + <link rel="stylesheet" href="test-bug-782653-css-errors-2.css"> + </head> + <body> + <p>Web Console test for bug 782653 : Open CSS Links in Style Editor.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-837351-security-errors.html b/browser/devtools/webconsole/test/test-bug-837351-security-errors.html new file mode 100644 index 000000000..db83274f0 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-837351-security-errors.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf8"> + <title>Mixed Content test - http on https</title> + <script src="testscript.js"></script> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe src = "http://example.com"></iframe> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html b/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html new file mode 100644 index 000000000..a2353354d --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html @@ -0,0 +1,13 @@ +<!doctype html> + <html> + <head> + <meta charset="utf8"> + <title>Bug 846918 - Report invalid strict-transport-security + headers to the web console</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>This page is served with an invalid STS header.</p> + </body> + </html> diff --git a/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html^headers^ b/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html^headers^ new file mode 100644 index 000000000..9778993d7 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html^headers^ @@ -0,0 +1 @@ +Strict-Transport-Security: max-age444
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html b/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html new file mode 100644 index 000000000..51bc0de28 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-859170-longstring-hang.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> + <head><meta charset="utf-8"> + <title>Web Console test for bug 859170 - very long strings hang the browser</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<script type="application/javascript"> +(function() { +var longString = "abbababazomglolztest"; +for (var i = 0; i < 10; i++) { + longString += longString + longString; +} + +longString = "foobar" + (new Array(9000)).join("a") + "foobaz" + + longString + "boom!"; +console.log(longString); +})(); +</script> + </head> + <body> + <p>Web Console test for bug 859170 - very long strings hang the browser.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-869003-iframe.html b/browser/devtools/webconsole/test/test-bug-869003-iframe.html new file mode 100644 index 000000000..5a29728e5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-869003-iframe.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 869003</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"><!-- + window.onload = function testConsoleLogging() + { + var o = { hello: "world!", bug: 869003 }; + console.log("foobar", o); + }; + // --></script> + </head> + <body> + <p>Make sure users can inspect objects from cross-domain iframes.</p> + <p>Iframe window.</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-869003-top-window.html b/browser/devtools/webconsole/test/test-bug-869003-top-window.html new file mode 100644 index 000000000..ab3b87542 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-869003-top-window.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 869003</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Make sure users can inspect objects from cross-domain iframes.</p> + <p>Top window.</p> + <iframe src="http://example.org/browser/browser/devtools/webconsole/test/test-bug-869003-iframe.html"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html b/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html new file mode 100644 index 000000000..de297d9b5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-bug-989025-iframe-parent.html b/browser/devtools/webconsole/test/test-bug-989025-iframe-parent.html new file mode 100644 index 000000000..e9a8553dd --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-989025-iframe-parent.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>test for bug 989025 - iframe parent</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>test for bug 989025 - iframe parent</p> + <iframe src="http://mochi.test:8888/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html"></iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug_923281_console_log_filter.html b/browser/devtools/webconsole/test/test-bug_923281_console_log_filter.html new file mode 100644 index 000000000..f2d650a5d --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug_923281_console_log_filter.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console test</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-bug_923281_test1.js"></script> + <script type="text/javascript" src="test-bug_923281_test2.js"></script> + </head> + <body></body> +</html> diff --git a/browser/devtools/webconsole/test/test-bug_923281_test1.js b/browser/devtools/webconsole/test/test-bug_923281_test1.js new file mode 100644 index 000000000..81342a437 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug_923281_test1.js @@ -0,0 +1,5 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +console.log("Sample log."); +console.log("This log should be filtered when filtered for test2.js."); diff --git a/browser/devtools/webconsole/test/test-bug_923281_test2.js b/browser/devtools/webconsole/test/test-bug_923281_test2.js new file mode 100644 index 000000000..f523103cd --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug_923281_test2.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +console.log("This is a random text."); diff --git a/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html b/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html new file mode 100644 index 000000000..ab44de09f --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 939783 - different console.trace() calls + wrongly filtered as duplicates</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<script type="application/javascript"> +function foo1() { + foo2(); +} + +function foo1b() { + foo2(); +} + +function foo2() { + foo3(); +} + +function foo3() { + console.trace(); +} + +foo1(); foo1(); +foo1b(); + +</script> + </head> + <body> + <p>Web Console test for bug 939783 - different console.trace() calls + wrongly filtered as duplicates</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-certificate-messages.html b/browser/devtools/webconsole/test/test-certificate-messages.html new file mode 100644 index 000000000..b0419a6fc --- /dev/null +++ b/browser/devtools/webconsole/test/test-certificate-messages.html @@ -0,0 +1,22 @@ +<!-- + Bug 1068949 - Log crypto warnings to the security pane in the webconsole +--> + +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Security warning test - no violations</title> + <!-- ensure no subresource errors so window re-use doesn't cause failures --> + <link rel="icon" href="data:;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"> + <script> + console.log("If you haven't seen ssl warnings yet, you won't"); + </script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-closure-optimized-out.html b/browser/devtools/webconsole/test/test-closure-optimized-out.html new file mode 100644 index 000000000..3ad4e8fc0 --- /dev/null +++ b/browser/devtools/webconsole/test/test-closure-optimized-out.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Debugger Test for Inspecting Optimized-Out Variables</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function clickHandler(event) { + button.removeEventListener("click", clickHandler, false); + function outer(arg) { + var upvar = arg * 2; + // The inner lambda only aliases arg, so the frontend alias analysis decides + // that upvar is not aliased and is not in the CallObject. + return function () { + arg += 2; + }; + } + + var f = outer(42); + f(); + } + var button = document.querySelector("button"); + button.addEventListener("click", clickHandler, false); + }); + </script> + + </head> + <body> + <button>Click me!</button> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-closures.html b/browser/devtools/webconsole/test/test-closures.html new file mode 100644 index 000000000..4fadade20 --- /dev/null +++ b/browser/devtools/webconsole/test/test-closures.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Console Test for Closure Inspection</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + function injectPerson() { + var PersonFactory = function _pfactory(name) { + var foo = 10; + return { + getName: function() { return name; }, + getFoo: function() { foo = Date.now(); return foo; } + }; + }; + window.george = new PersonFactory("George"); + debugger; + } + </script> + + </head> + <body> + <button onclick="injectPerson()">Test</button> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-api-stackframe.html b/browser/devtools/webconsole/test/test-console-api-stackframe.html new file mode 100644 index 000000000..df7fef9b1 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-api-stackframe.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 920116 - stacktraces for console API messages</title> + <script> + function firstCall() { + secondCall(); + } + + function secondCall() { + thirdCall(); + } + + function thirdCall() { + console.log("foo-log"); + console.error("foo-error"); + console.exception("foo-exception"); + console.assert("red" == "blue", "foo-assert"); + } + + window.onload = firstCall; + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-assert.html b/browser/devtools/webconsole/test/test-console-assert.html new file mode 100644 index 000000000..b104d72d4 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-assert.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + <meta charset="utf-8"> + <title>console.assert() test</title> + <script type="text/javascript"> + function test() { + console.log("start"); + console.assert(false, "false assert"); + console.assert(0, "falsy assert"); + console.assert(true, "true assert"); + console.log("end"); + } + </script> + </head> + <body> + <p>test console.assert()</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-column.html b/browser/devtools/webconsole/test/test-console-column.html new file mode 100644 index 000000000..ff9cc81e1 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-column.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <meta charset="utf-8"> + <title>Console test</title> + + <script type="text/javascript"> + console.info("INLINE SCRIPT:"); console.log('Further'); + console.warn("I'm warning you, he will eat up all yr bacon."); + console.error("Error Message"); + console.log('Rainbooooww'); + console.log('NYAN CATZ'); + </script> + </head> +</html> diff --git a/browser/devtools/webconsole/test/test-console-count-external-file.js b/browser/devtools/webconsole/test/test-console-count-external-file.js new file mode 100644 index 000000000..77959b831 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-count-external-file.js @@ -0,0 +1,7 @@ +function counterExternalFile() { + console.count("console.count() testcounter"); +} +function externalCountersWithoutLabel() { + console.count(); + console.count(); +} diff --git a/browser/devtools/webconsole/test/test-console-count.html b/browser/devtools/webconsole/test/test-console-count.html new file mode 100644 index 000000000..e6db0ebb0 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-count.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + <meta charset="utf-8"> + <title>console.count() test</title> + <script src="test-console-count-external-file.js"></script> + <script tyoe="text/javascript"> + function counterSeperateScriptTag() { + console.count("console.count() testcounter"); + } + </script> + <script type="text/javascript"> + function counterNoLabel() { + console.count(); + } + function countersWithoutLabel() { + console.count(); + console.count(); + } + function counterWithLabel() { + console.count("console.count() testcounter"); + } + function testLocal() { + console.log("start"); + counterNoLabel(); + counterNoLabel(); + countersWithoutLabel(); + counterWithLabel(); + counterWithLabel(); + counterSeperateScriptTag(); + counterSeperateScriptTag(); + console.log("end"); + } + function testExternal() { + console.log("start"); + counterExternalFile(); + counterExternalFile(); + externalCountersWithoutLabel(); + console.log("end"); + } + </script> + </head> + <body> + <p>test console.count()</p> + <button id="local" onclick="testLocal();"> + test local console.count() calls + </button> + <button id="external" onclick="testExternal();"> + test external console.count() calls + </button> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-extras.html b/browser/devtools/webconsole/test/test-console-extras.html new file mode 100644 index 000000000..ae0b521c5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-extras.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console extended API test</title> + <script type="text/javascript"> + function test() { + console.log("start"); + console.clear() + console.dirxml() + console.log("end"); + } + </script> + </head> + <body> + <h1 id="header">Heads Up Display Demo</h1> + <button onclick="test();">Test Extended API</button> + <div id="myDiv"></div> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-output-02.html b/browser/devtools/webconsole/test/test-console-output-02.html new file mode 100644 index 000000000..c2fe21233 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-02.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - 02</title> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> + <p>hello world!</p> + <script type="text/javascript"> +function testfn1() { return 42; } + +var testobj1 = { + testfn2: function() { return 42; }, +}; + +function testfn3() { return 42; } +testfn3.displayName = "testfn3DisplayName"; + +var array1 = [1, 2, 3, "a", "b", "c", "4", "5"]; + +var array2 = ["a", document, document.body, document.body.dataset, + document.body.classList]; + +var array3 = [1, window, null, "a", "b", undefined, false, "", -Infinity, testfn3, testobj1, "foo", "bar"]; + +var array4 = new Array(5); +array4.push("test"); +array4.push(array4); + +var typedarray1 = new Int32Array([1, 287, 8651, 40983, 8754]); + +var set1 = new Set([1, 2, null, array3, "a", "b", undefined, document.head]); +set1.add(set1); + +var testobj2 = {a: "b", c: "d", e: 1, f: "2"}; +testobj2.foo = testobj1; +testobj2.bar = testobj2; +Object.defineProperty(testobj2, "getterTest", { + enumerable: true, + get: function() { + return 42; + }, +}); + +var testobj3 = {a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined, + j: "", k: document.styleSheets, l: document.body.childNodes, + o: new Array(125), m: document.head}; + +var testobj4 = {a: "b", c: "d"}; +Object.defineProperty(testobj4, "nonEnumerable", { value: "hello world" }); + +var map1 = new Map([["a", "b"], [document.body.children, testobj2]]); +map1.set(map1, set1); + + </script> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-output-03.html b/browser/devtools/webconsole/test/test-console-output-03.html new file mode 100644 index 000000000..9dcf051a6 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-03.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - 03</title> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> + <p>hello world!</p> + <script type="text/javascript"> +function testBodyClassName() { + document.body.className = "test1 tezt2"; + return document.body; +} + +function testBodyID() { + document.body.id = 'foobarid'; + return document.body; +} + +function testBodyDataset() { + document.body.dataset.preview = 'zuzu"<a>foo'; + return document.body; +} + </script> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-output-04.html b/browser/devtools/webconsole/test/test-console-output-04.html new file mode 100644 index 000000000..bb4345277 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-04.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - 04</title> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> + <p>hello world!</p> + <script type="text/javascript"> +function testTextNode() { + return document.querySelector("p").childNodes[0]; +} + +function testCommentNode() { + return document.head.childNodes[5]; +} + +function testDocumentFragment() { + var frag = document.createDocumentFragment(); + + var div = document.createElement("div"); + div.id = "foo1"; + div.className = "bar"; + frag.appendChild(div); + + var span = document.createElement("span"); + span.id = "foo2"; + span.textContent = "hello world"; + div.appendChild(span); + + var div2 = document.createElement("div"); + div2.id = "foo3"; + frag.appendChild(div2); + + return frag; +} + +function testError() { + try { + window.foobar("a"); + } catch (ex) { + return ex; + } + return null; +} + +function testDOMException() { + try { + var foo = document.querySelector("foo;()bar!"); + } catch (ex) { + return ex; + } + return null; +} + +function testCSSStyleDeclaration() { + document.body.style = 'color: green; font-size: 2em'; + return document.body.style; +} + +function testStyleSheetList() { + var style = document.querySelector("style"); + if (!style) { + style = document.createElement("style"); + style.textContent = "p, div { color: blue; font-weight: bold }\n" + + "@media print { p { background-color: yellow } }"; + document.head.appendChild(style); + } + return document.styleSheets; +} + </script> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-output-dom-elements.html b/browser/devtools/webconsole/test/test-console-output-dom-elements.html new file mode 100644 index 000000000..84ec58a7b --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-dom-elements.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - 05</title> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body class="body-class" id="body-id"> + <p some-attribute="some-value">hello world!</p> + <iframe src="data:text/html,<p>hello from iframe</p>"></iframe> + <div class="some classname here with more classnames here"></div> + <script type="text/javascript"> +function testBodyNode() { + return document.body; +} + +function testDocumentElement() { + return document.documentElement; +} + +function testDocument() { + return document; +} + +function testNode() { + return document.querySelector("p"); +} + +function testNodeList() { + return document.querySelectorAll("*"); +} + +function testNodeInIframe() { + return document.querySelector("iframe").contentWindow.document.querySelector("p"); +} + +function testDocumentFragment() { + var frag = document.createDocumentFragment(); + + var span = document.createElement("span"); + span.className = 'foo'; + span.dataset.lolz = 'hehe'; + + var div = document.createElement('div') + div.id = 'fragdiv'; + + frag.appendChild(span); + frag.appendChild(div); + + return frag; +} + +function testNodeInDocumentFragment() { + var frag = testDocumentFragment(); + return frag.firstChild; +} + +function testUnattachedNode() { + var p = document.createElement("p"); + p.className = "such-class"; + p.dataset.data = "such-data"; + return p; +} + </script> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-output-events.html b/browser/devtools/webconsole/test/test-console-output-events.html new file mode 100644 index 000000000..908a86fab --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-events.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output for DOM events</title> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> + <p>hello world!</p> + + <script type="text/javascript"> +function testDOMEvents() { + function eventLogger(ev) { + console.log("eventLogger", ev); + } + document.addEventListener("mousemove", eventLogger); + document.addEventListener("keypress", eventLogger); + + synthesizeMouseMove(); + synthesizeKeyPress("a", {shiftKey: true}); +} + +function synthesizeMouseMove(element) { + var mouseEvent = document.createEvent("MouseEvent"); + mouseEvent.initMouseEvent("mousemove", true, true, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + + document.dispatchEvent(mouseEvent); +} + +function synthesizeKeyPress(key, options) { + var keyboardEvent = document.createEvent("KeyboardEvent"); + keyboardEvent.initKeyEvent("keypress", true, true, window, false, false, + options.shiftKey, false, key.charCodeAt(0), 0); + document.dispatchEvent(keyboardEvent); +} + </script> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-replaced-api.html b/browser/devtools/webconsole/test/test-console-replaced-api.html new file mode 100644 index 000000000..2b05d023a --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-replaced-api.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console test replaced API</title> + </head> + <body> + <h1 id="header">Web Console Replace API Test</h1> + <script type="text/javascript"> + window.console = {log: function (msg){}, info: function (msg){}, warn: function (msg){}, error: function (msg){}}; + </script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console-table.html b/browser/devtools/webconsole/test/test-console-table.html new file mode 100644 index 000000000..7a3f2333e --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-table.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for Bug 899753 - console.table support</title> + <script> + var languages1 = [ + { name: "JavaScript", fileExtension: [".js"] }, + { name: { a: "TypeScript" }, fileExtension: ".ts" }, + { name: "CoffeeScript", fileExtension: ".coffee" } + ]; + + var languages2 = { + csharp: { name: "C#", paradigm: "object-oriented" }, + fsharp: { name: "F#", paradigm: "functional" } + }; + + function Person(firstName, lastName, age) + { + this.firstName = firstName; + this.lastName = lastName; + this.age = age; + } + + var family = {}; + family.mother = new Person("Susan", "Doyle", 32); + family.father = new Person("John", "Doyle", 33); + family.daughter = new Person("Lily", "Doyle", 5); + family.son = new Person("Mike", "Doyle", 8); + + var myMap = new Map(); + + myMap.set("a string", "value associated with 'a string'"); + myMap.set(5, "value associated with 5"); + + var mySet = new Set(); + + mySet.add(1); + mySet.add(5); + mySet.add("some text"); + mySet.add(null); + mySet.add(undefined); + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-console.html b/browser/devtools/webconsole/test/test-console.html new file mode 100644 index 000000000..27df226e4 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console test</title> + <script type="text/javascript"> + function test() { + var str = "Dolske Digs Bacon, Now and Forevermore." + for (var i=0; i < 5; i++) { + console.log(str); + } + } + console.info("INLINE SCRIPT:"); + test(); + console.warn("I'm warning you, he will eat up all yr bacon."); + console.error("Error Message"); + </script> + </head> + <body> + <h1 id="header">Heads Up Display Demo</h1> + <button onclick="test();">Log stuff about Dolske</button> + <div id="myDiv"></div> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-consoleiframes.html b/browser/devtools/webconsole/test/test-consoleiframes.html new file mode 100644 index 000000000..a8176f93a --- /dev/null +++ b/browser/devtools/webconsole/test/test-consoleiframes.html @@ -0,0 +1,13 @@ +<html> +<head> + <script> + console.log("main file"); + </script> +</head> +<body> +<h1>iframe console test</h1> +<iframe src="test-iframe1.html"></iframe> +<iframe src="test-iframe2.html"></iframe> +<iframe src="test-iframe3.html"></iframe> +</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-data.json b/browser/devtools/webconsole/test/test-data.json new file mode 100644 index 000000000..471d240b5 --- /dev/null +++ b/browser/devtools/webconsole/test/test-data.json @@ -0,0 +1 @@ +{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-data.json^headers^ b/browser/devtools/webconsole/test/test-data.json^headers^ new file mode 100644 index 000000000..7b5e82d4b --- /dev/null +++ b/browser/devtools/webconsole/test/test-data.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json diff --git a/browser/devtools/webconsole/test/test-duplicate-error.html b/browser/devtools/webconsole/test/test-duplicate-error.html new file mode 100644 index 000000000..1b2691672 --- /dev/null +++ b/browser/devtools/webconsole/test/test-duplicate-error.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console duplicate error test</title> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + + See https://bugzilla.mozilla.org/show_bug.cgi?id=582201 + --> + </head> + <body> + <h1>Heads Up Display - duplicate error test</h1> + + <script type="text/javascript"><!-- + fooDuplicateError1.bar(); + // --></script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html b/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html new file mode 100644 index 000000000..cf19629f4 --- /dev/null +++ b/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="ISO-8859-1"> +</head> +<body>üöä</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-error.html b/browser/devtools/webconsole/test/test-error.html new file mode 100644 index 000000000..abf62a3f1 --- /dev/null +++ b/browser/devtools/webconsole/test/test-error.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console error test</title> + </head> + <body> + <h1>Heads Up Display - error test</h1> + <p><button>generate error</button></p> + + <script type="text/javascript"><!-- + var button = document.getElementsByTagName("button")[0]; + + button.addEventListener("click", function clicker () { + button.removeEventListener("click", clicker, false); + fooBazBaz.bar(); + }, false); + // --></script> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-eval-in-stackframe.html b/browser/devtools/webconsole/test/test-eval-in-stackframe.html new file mode 100644 index 000000000..ec1bf3f30 --- /dev/null +++ b/browser/devtools/webconsole/test/test-eval-in-stackframe.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 783499 - use the debugger API in the web console</title> + <script> + var foo = "globalFooBug783499"; + var fooObj = { + testProp: "testValue", + }; + + function firstCall() + { + var foo = "fooFirstCall"; + var foo3 = "foo3FirstCall"; + secondCall(); + } + + function secondCall() + { + var foo2 = "foo2SecondCall"; + var fooObj = { + testProp2: "testValue2", + }; + var fooObj2 = { + testProp22: "testValue22", + }; + debugger; + } + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-file-location.js b/browser/devtools/webconsole/test/test-file-location.js new file mode 100644 index 000000000..f97ce5725 --- /dev/null +++ b/browser/devtools/webconsole/test/test-file-location.js @@ -0,0 +1,9 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +console.log("message for level log"); +console.info("message for level info"); +console.warn("message for level warn"); +console.error("message for level error"); +console.debug("message for level debug"); diff --git a/browser/devtools/webconsole/test/test-filter.html b/browser/devtools/webconsole/test/test-filter.html new file mode 100644 index 000000000..219177bb2 --- /dev/null +++ b/browser/devtools/webconsole/test/test-filter.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console test</title> + <script type="text/javascript"> + </script> + </head> + <body> + <h1>Heads Up Display Filter Test Page</h1> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-for-of.html b/browser/devtools/webconsole/test/test-for-of.html new file mode 100644 index 000000000..876010c9e --- /dev/null +++ b/browser/devtools/webconsole/test/test-for-of.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<body> +<h1>a</h1> +<div><p>b</p></div> +<h2>c</h2> +<p>d</p> diff --git a/browser/devtools/webconsole/test/test-iframe-762593-insecure-form-action.html b/browser/devtools/webconsole/test/test-iframe-762593-insecure-form-action.html new file mode 100644 index 000000000..d14b5cdd7 --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe-762593-insecure-form-action.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <h1>iframe 2</h1> + <p>This frame contains a password field inside a form with insecure action.</p> + <form action="http://test"> + <input type="password" name="pwd"> + </form> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-iframe-762593-insecure-frame.html b/browser/devtools/webconsole/test/test-iframe-762593-insecure-frame.html new file mode 100644 index 000000000..505676acb --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe-762593-insecure-frame.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <h1>iframe 1</h1> + <p>This frame is served with an insecure password field.</p> + <iframe src= + "http://example.com/browser/browser/devtools/webconsole/test/test-iframe-762593-insecure-form-action.html"> + </iframe> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-iframe1.html b/browser/devtools/webconsole/test/test-iframe1.html new file mode 100644 index 000000000..4dd4eddfe --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe1.html @@ -0,0 +1,10 @@ +<html> +<head> + <script> + console.log("iframe 1"); + </script> +</head> +<body> +<h1>iframe 1</h1> +</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-iframe2.html b/browser/devtools/webconsole/test/test-iframe2.html new file mode 100644 index 000000000..c15884795 --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe2.html @@ -0,0 +1,11 @@ +<html> +<head> + <script> + console.log("iframe 2"); + blah; + </script> +</head> +<body> +<h1>iframe 2</h1> +</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-iframe3.html b/browser/devtools/webconsole/test/test-iframe3.html new file mode 100644 index 000000000..f0df8b669 --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe3.html @@ -0,0 +1,11 @@ +<html> +<head> + <script> + console.log("iframe 3"); + </script> +</head> +<body> +<h1>iframe 3</h1> +<iframe src="test-iframe1.html"></iframe> +</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-image.png b/browser/devtools/webconsole/test/test-image.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/browser/devtools/webconsole/test/test-image.png diff --git a/browser/devtools/webconsole/test/test-mixedcontent-securityerrors.html b/browser/devtools/webconsole/test/test-mixedcontent-securityerrors.html new file mode 100644 index 000000000..cb8cfdaaf --- /dev/null +++ b/browser/devtools/webconsole/test/test-mixedcontent-securityerrors.html @@ -0,0 +1,21 @@ +<!-- + Bug 875456 - Log mixed content messages from the Mixed Content Blocker to the + Security Pane in the Web Console +--> + +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Mixed Content test - http on https</title> + <script src="testscript.js"></script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <iframe src="http://example.com"></iframe> + <img src="http://example.com/tests/image/test/mochitest/blue.png"></img> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-mutation.html b/browser/devtools/webconsole/test/test-mutation.html new file mode 100644 index 000000000..e80933b06 --- /dev/null +++ b/browser/devtools/webconsole/test/test-mutation.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console mutation test</title> + <script> + window.onload = function (){ + var node = document.createElement("div"); + document.body.appendChild(node); + }; + </script> + </head> + <body> + <h1>Heads Up Display DOM Mutation Test Page</h1> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-network-request.html b/browser/devtools/webconsole/test/test-network-request.html new file mode 100644 index 000000000..f8f75d60b --- /dev/null +++ b/browser/devtools/webconsole/test/test-network-request.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Console HTTP test page</title> + <script type="text/javascript"><!-- + function makeXhr(aMethod, aUrl, aRequestBody, aCallback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open(aMethod, aUrl, true); + xmlhttp.onreadystatechange = function() { + if (aCallback && xmlhttp.readyState == 4) { + aCallback(); + } + }; + xmlhttp.send(aRequestBody); + } + + function testXhrGet(aCallback) { + makeXhr('get', 'test-data.json', null, aCallback); + } + + function testXhrWarn(aCallback) { + makeXhr('get', 'http://example.com/browser/browser/devtools/netmonitor/test/sjs_cors-test-server.sjs', null, aCallback); + } + + function testXhrPost(aCallback) { + makeXhr('post', 'test-data.json', "Hello world!", aCallback); + } + // --></script> + </head> + <body> + <h1>Heads Up Display HTTP Logging Testpage</h1> + <h2>This page is used to test the HTTP logging.</h2> + + <form action="https://example.com/browser/browser/devtools/webconsole/test/test-network-request.html" method="post"> + <input name="name" type="text" value="foo bar"><br> + <input name="age" type="text" value="144"><br> + </form> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-network.html b/browser/devtools/webconsole/test/test-network.html new file mode 100644 index 000000000..69d3422e3 --- /dev/null +++ b/browser/devtools/webconsole/test/test-network.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console network test</title> + <script src="testscript.js?foo"></script> + </head> + <body> + <h1>Heads Up Display Network Test Page</h1> + <img src="test-image.png"></img> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-observe-http-ajax.html b/browser/devtools/webconsole/test/test-observe-http-ajax.html new file mode 100644 index 000000000..5abcefdad --- /dev/null +++ b/browser/devtools/webconsole/test/test-observe-http-ajax.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console HTTP test page</title> + <script type="text/javascript"> + function test() { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open('get', 'test-data.json', false); + xmlhttp.send(null); + } + </script> + </head> + <body onload="test();"> + <h1>Heads Up Display HTTP & AJAX Test Page</h1> + <h2>This page fires an ajax request so we can see the http logging of the console</h2> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-own-console.html b/browser/devtools/webconsole/test/test-own-console.html new file mode 100644 index 000000000..d1d18ebc2 --- /dev/null +++ b/browser/devtools/webconsole/test/test-own-console.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> +<head> +<meta charset="utf-8"> +<script> + var _console = { + foo: "bar" + } + + window.console = _console; + + function loadIFrame() { + var iframe = document.body.querySelector("iframe"); + iframe.addEventListener("load", function() { + iframe.removeEventListener("load", arguments.callee, true); + }, true); + + iframe.setAttribute("src", "test-console.html"); + } +</script> +</head> +<body> + <iframe></iframe> +</body> diff --git a/browser/devtools/webconsole/test/test-property-provider.html b/browser/devtools/webconsole/test/test-property-provider.html new file mode 100644 index 000000000..532b00f44 --- /dev/null +++ b/browser/devtools/webconsole/test/test-property-provider.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Property provider test</title> + <script> + var testObj = { + testProp: 'testValue' + }; + </script> + </head> + <body> + <h1>Heads Up Property Provider Test Page</h1> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-repeated-messages.html b/browser/devtools/webconsole/test/test-repeated-messages.html new file mode 100644 index 000000000..97f5482e4 --- /dev/null +++ b/browser/devtools/webconsole/test/test-repeated-messages.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Test for bugs 720180, 800510 and 865288</title> + <script> + function testConsole() { + // same line and column number + for(var i = 0; i < 2; i++) { + console.log("foo repeat"); + } + console.log("foo repeat"); console.error("foo repeat"); + } + function testConsoleObjects() { + for (var i = 0; i < 3; i++) { + var o = { id: "abba" + i }; + console.log("abba", o); + } + } + </script> + <style> + body { + background-image: foobarz; + } + p { + background-image: foobarz; + } + </style> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + </head> + <body> + <p>Hello world!</p> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test-result-format-as-string.html b/browser/devtools/webconsole/test/test-result-format-as-string.html new file mode 100644 index 000000000..c3ab78ee7 --- /dev/null +++ b/browser/devtools/webconsole/test/test-result-format-as-string.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test: jsterm eval format as a string</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Make sure js eval results are formatted as strings.</p> + <script> + document.querySelector("p").toSource = function() { + var element = document.createElement("div"); + element.id = "foobar"; + element.textContent = "bug772506_content"; + element.setAttribute("onmousemove", + "(function () {" + + " gBrowser._bug772506 = 'foobar';" + + "})();" + ); + return element; + }; + </script> + </body> +</html> diff --git a/browser/devtools/webconsole/test/test-webconsole-error-observer.html b/browser/devtools/webconsole/test/test-webconsole-error-observer.html new file mode 100644 index 000000000..8466bc6f2 --- /dev/null +++ b/browser/devtools/webconsole/test/test-webconsole-error-observer.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>WebConsoleErrorObserver test - bug 611032</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + console.log("log Bazzle"); + console.info("info Bazzle"); + console.warn("warn Bazzle"); + console.error("error Bazzle"); + + var foo = {}; + foo.bazBug611032(); + </script> + <style type="text/css"> + .foo { color: cssColorBug611032; } + </style> + </head> + <body> + <h1>WebConsoleErrorObserver test</h1> + </body> +</html> + diff --git a/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html new file mode 100644 index 000000000..4872a1df7 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Bug 1045902 - CSP: Log console message for 'reflected-xss'</title> +</head> +<body> +Bug 1045902 - CSP: Log console message for 'reflected-xss' +</body> +</html> diff --git a/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ new file mode 100644 index 000000000..0b234f0e8 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: reflected-xss filter; diff --git a/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.html b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.html new file mode 100644 index 000000000..ebb7773cb --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Bug 1092055 - Log console messages for non-top-level security errors</title> + <script src="test_bug1092055_shouldwarn.js"></script> + <!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + --> +</head> +<body> +Bug 1092055 - Log console messages for non-top-level security errors +</body> +</html> diff --git a/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js new file mode 100644 index 000000000..c7d5cec14 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js @@ -0,0 +1,2 @@ +// It doesn't matter what this script does, but the broken HSTS header sent +// with it should result in warnings in the webconsole diff --git a/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js^headers^ b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js^headers^ new file mode 100644 index 000000000..f99377fc6 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1092055_shouldwarn.js^headers^ @@ -0,0 +1 @@ +Strict-Transport-Security: some complete nonsense diff --git a/browser/devtools/webconsole/test/test_bug_1010953_cspro.html b/browser/devtools/webconsole/test/test_bug_1010953_cspro.html new file mode 100644 index 000000000..83ac6391f --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug_1010953_cspro.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Test for Bug 1010953 - Verify that CSP and CSPRO log different console +messages.</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010953">Mozilla Bug 1010953</a> + + +<!-- this script file allowed by the CSP header (but not by the report-only header) --> +<script src="http://some.example.com/test_bug_1010953_cspro.js"></script> + +<!-- this image allowed only be the CSP report-only header. --> +<img src="http://some.example.com/test.png"> +</body> +</html>
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test_bug_1010953_cspro.html^headers^ b/browser/devtools/webconsole/test/test_bug_1010953_cspro.html^headers^ new file mode 100644 index 000000000..03056e2cb --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug_1010953_cspro.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src 'self'; img-src 'self'; script-src some.example.com; +Content-Security-Policy-Report-Only: default-src 'self'; img-src some.example.com; script-src 'self'; report-uri https://example.com/ignored/;
\ No newline at end of file diff --git a/browser/devtools/webconsole/test/test_bug_770099_violation.html b/browser/devtools/webconsole/test/test_bug_770099_violation.html new file mode 100644 index 000000000..ccbded87a --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug_770099_violation.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Test for Bug 770099 - policy violation</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=770099">Mozilla Bug 770099</a> +<img src="http://some.example.com/test.png"> +</body> +</html> diff --git a/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^ b/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug_770099_violation.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/browser/devtools/webconsole/test/testscript.js b/browser/devtools/webconsole/test/testscript.js new file mode 100644 index 000000000..c69919df4 --- /dev/null +++ b/browser/devtools/webconsole/test/testscript.js @@ -0,0 +1 @@ +console.log("running network console logging tests"); diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index e9a5b277e..3c7d022b0 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -1,4 +1,4 @@ -/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ /* 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 @@ -6,54 +6,45 @@ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", - "@mozilla.org/widget/clipboardhelper;1", - "nsIClipboardHelper"); - -XPCOMUtils.defineLazyModuleGetter(this, "GripClient", - "resource://gre/modules/devtools/dbg-client.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel", - "resource:///modules/NetworkPanel.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", - "resource:///modules/devtools/AutocompletePopup.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource://gre/modules/devtools/WebConsoleUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "promise", - "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise"); - -XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", - "resource:///modules/devtools/VariablesView.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController", - "resource:///modules/devtools/VariablesViewController.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource:///modules/devtools/shared/event-emitter.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "devtools", - "resource://gre/modules/devtools/Loader.jsm"); +const {Cc, Ci, Cu} = require("chrome"); + +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; + +loader.lazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); +loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); +loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter")); +loader.lazyGetter(this, "AutocompletePopup", + () => require("devtools/shared/autocomplete-popup").AutocompletePopup); +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, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm"); +loader.lazyImporter(this, "ObjectClient", "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"); +loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent"; + +const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; -// The XUL namespace. -const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; -const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent"; +const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm"; const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; @@ -61,11 +52,7 @@ const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesV const CONSOLE_DIR_VIEW_HEIGHT = 0.6; -const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; - -// The amount of time in milliseconds that must pass between messages to -// trigger the display of a new group. -const NEW_GROUP_DELAY = 5000; +const IGNORED_SOURCE_URLS = ["debugger eval code"]; // The amount of time in milliseconds that we wait before performing a live // search. @@ -118,31 +105,35 @@ const SEVERITY_CLASS_FRAGMENTS = [ // Most of these rather idiosyncratic names are historical and predate the // division of message type into "category" and "severity". const MESSAGE_PREFERENCE_KEYS = [ -// Error Warning Info Log - [ "network", null, null, "networkinfo", ], // Network - [ "csserror", "cssparser", null, null, ], // CSS - [ "exception", "jswarn", null, "jslog", ], // JS - [ "error", "warn", "info", "log", ], // Web Developer - [ null, null, null, null, ], // Input - [ null, null, null, null, ], // Output - [ "secerror", "secwarn", null, null, ], // Security +// Error Warning Info Log + [ "network", "netwarn", "netxhr", "networkinfo", ], // Network + [ "csserror", "cssparser", null, "csslog", ], // CSS + [ "exception", "jswarn", null, "jslog", ], // JS + [ "error", "warn", "info", "log", ], // Web Developer + [ null, null, null, null, ], // Input + [ null, null, null, null, ], // Output + [ "secerror", "secwarn", null, null, ], // Security ]; // A mapping from the console API log event levels to the Web Console // severities. const LEVELS = { error: SEVERITY_ERROR, + exception: SEVERITY_ERROR, + assert: SEVERITY_ERROR, warn: SEVERITY_WARNING, info: SEVERITY_INFO, log: SEVERITY_LOG, trace: SEVERITY_LOG, + table: SEVERITY_LOG, debug: SEVERITY_LOG, dir: SEVERITY_LOG, group: SEVERITY_LOG, groupCollapsed: SEVERITY_LOG, groupEnd: SEVERITY_LOG, time: SEVERITY_LOG, - timeEnd: SEVERITY_LOG + timeEnd: SEVERITY_LOG, + count: SEVERITY_LOG }; // The lowest HTTP response code (inclusive) that is considered an error. @@ -158,13 +149,18 @@ const HISTORY_FORWARD = 1; const GROUP_INDENT = 12; // 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. +// too many messages at once we slow down the Firefox UI too much. const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; // The delay between display updates - tells how often we should *try* to push // new messages to screen. This value is optimistic, updates won't always // happen. Keep this low so the Web Console output feels live. -const OUTPUT_INTERVAL = 50; // milliseconds +const OUTPUT_INTERVAL = 20; // milliseconds + +// The maximum amount of time that can be spent doing cleanup inside of the +// flush output callback. If things don't get cleaned up in this time, +// then it will start again the next time it is called. +const MAX_CLEANUP_TIME = 10; // milliseconds // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle // output updates to this number of milliseconds. So during a lot of output we @@ -177,11 +173,9 @@ const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; // The minimum font size. const MIN_FONT_SIZE = 10; -// The maximum length of strings to be displayed by the Web Console. -const MAX_LONG_STRING_LENGTH = 200000; - const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; /** * A WebConsoleFrame instance is an interactive console initialized *per target* @@ -191,6 +185,7 @@ const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; * The WebConsoleFrame is responsible for the actual Web Console UI * implementation. * + * @constructor * @param object aWebConsoleOwner * The WebConsole owner object. */ @@ -198,26 +193,33 @@ function WebConsoleFrame(aWebConsoleOwner) { this.owner = aWebConsoleOwner; this.hudId = this.owner.hudId; + this.window = this.owner.iframeWindow; this._repeatNodes = {}; this._outputQueue = []; + this._itemDestroyQueue = []; this._pruneCategoriesQueue = {}; this._networkRequests = {}; this.filterPrefs = {}; + this.output = new ConsoleOutput(this); + this._toggleFilter = this._toggleFilter.bind(this); + this._onPanelSelected = this._onPanelSelected.bind(this); this._flushMessageQueue = this._flushMessageQueue.bind(this); + this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._outputTimerInitialized = false; EventEmitter.decorate(this); } +exports.WebConsoleFrame = WebConsoleFrame; WebConsoleFrame.prototype = { /** * The WebConsole instance that owns this frame. - * @see HUDService.jsm::WebConsole + * @see hudservice.js::WebConsole * @type object */ owner: null, @@ -342,6 +344,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 */ @@ -358,6 +366,11 @@ WebConsoleFrame.prototype = { // Used in tests. _saveRequestAndResponseBodies: false, + // Chevron width at the starting of Web Console's input box. + _chevronWidth: 0, + // Width of the monospace characters in Web Console's input box. + _inputCharWidth: 0, + /** * Tells whether to save the bodies of network requests and responses. * Disabled by default to save memory. @@ -394,6 +407,11 @@ WebConsoleFrame.prototype = { */ setSaveRequestAndResponseBodies: function WCF_setSaveRequestAndResponseBodies(aValue) { + if (!this.webConsoleClient) { + // Don't continue if the webconsole disconnected. + return promise.resolve(null); + } + let deferred = promise.defer(); let newValue = !!aValue; let toSet = { @@ -414,24 +432,18 @@ WebConsoleFrame.prototype = { return deferred.promise; }, - _persistLog: null, - /** - * Getter for the persistent logging preference. This value is cached per - * instance to avoid reading the pref too often. + * Getter for the persistent logging preference. * @type boolean */ get persistLog() { - if (this._persistLog === null) { - this._persistLog = Services.prefs.getBoolPref(PREF_PERSISTLOG); - } - return this._persistLog; + return Services.prefs.getBoolPref(PREF_PERSISTLOG); }, /** * Initialize the WebConsoleFrame instance. * @return object - * A Promise object for the initialization. + * A promise object for the initialization. */ init: function WCF_init() { @@ -444,7 +456,7 @@ WebConsoleFrame.prototype = { * * @private * @return object - * A Promise object that is resolved/reject based on the connection + * A promise object that is resolved/reject based on the connection * result. */ _initConnection: function WCF__initConnection() @@ -461,7 +473,7 @@ WebConsoleFrame.prototype = { }, (aReason) => { // on failure let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, aReason.error + ": " + aReason.message); - this.outputMessage(CATEGORY_JS, node); + this.outputMessage(CATEGORY_JS, node, [aReason]); this._initDefer.reject(aReason); }).then(() => { let id = WebConsoleUtils.supportsString(this.hudId); @@ -477,9 +489,6 @@ WebConsoleFrame.prototype = { */ _initUI: function WCF__initUI() { - // Remember that this script is loaded in the webconsole.xul context: - // |window| is the iframe global. - this.window = window; this.document = this.window.document; this.rootElement = this.document.documentElement; @@ -489,17 +498,20 @@ WebConsoleFrame.prototype = { this._commandController = new CommandController(this); this.window.controllers.insertControllerAt(0, this._commandController); + this._contextMenuHandler = new ConsoleContextMenu(this); + let doc = this.document; this.filterBox = doc.querySelector(".hud-filter-box"); - this.outputNode = doc.querySelector(".hud-output-node"); + this.outputNode = doc.getElementById("output-container"); this.completeNode = doc.querySelector(".jsterm-complete-node"); this.inputNode = doc.querySelector(".jsterm-input-node"); this._setFilterTextBoxEvents(); this._initFilterButtons(); - let fontSize = Services.prefs.getIntPref("devtools.webconsole.fontSize"); + let fontSize = this.owner._browserConsole ? + Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0; if (fontSize != 0) { fontSize = Math.max(MIN_FONT_SIZE, fontSize); @@ -509,6 +521,17 @@ WebConsoleFrame.prototype = { this.inputNode.style.fontSize = fontSize + "px"; } + if (this.owner._browserConsole) { + for (let id of ["Enlarge", "Reduce", "Reset"]) { + this.document.getElementById("cmd_fullZoom" + id) + .removeAttribute("disabled"); + } + } + + // Update the character width and height needed for the popup offset + // calculations. + this._updateCharSize(); + let updateSaveBodiesPrefUI = (aElement) => { this.getSaveRequestAndResponseBodies().then(aValue => { aElement.setAttribute("checked", aValue); @@ -524,25 +547,29 @@ WebConsoleFrame.prototype = { }); } + let saveBodiesDisabled = !this.getFilterState("networkinfo") && + !this.getFilterState("netxhr") && + !this.getFilterState("network"); + let saveBodies = doc.getElementById("saveBodies"); - saveBodies.addEventListener("click", reverseSaveBodiesPref); - saveBodies.disabled = !this.getFilterState("networkinfo") && - !this.getFilterState("network"); + saveBodies.addEventListener("command", reverseSaveBodiesPref); + saveBodies.disabled = saveBodiesDisabled; let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu"); - saveBodiesContextMenu.addEventListener("click", reverseSaveBodiesPref); - saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && - !this.getFilterState("network"); + saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref); + saveBodiesContextMenu.disabled = saveBodiesDisabled; saveBodies.parentNode.addEventListener("popupshowing", () => { updateSaveBodiesPrefUI(saveBodies); saveBodies.disabled = !this.getFilterState("networkinfo") && + !this.getFilterState("netxhr") && !this.getFilterState("network"); }); saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => { updateSaveBodiesPrefUI(saveBodiesContextMenu); saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && + !this.getFilterState("netxhr") && !this.getFilterState("network"); }); @@ -554,6 +581,42 @@ WebConsoleFrame.prototype = { this.jsterm = new JSTerm(this); this.jsterm.init(); + + let toolbox = gDevTools.getToolbox(this.owner.target); + if (toolbox) { + toolbox.on("webconsole-selected", this._onPanelSelected); + } + + /* + * Focus input line whenever the output area is clicked. + * Reusing _addMEssageLinkCallback since it correctly filters + * drag and select events. + */ + this._addFocusCallback(this.outputNode, (evt) => { + if ((evt.target.nodeName.toLowerCase() != "a") && + (evt.target.parentNode.nodeName.toLowerCase() != "a")) { + this.jsterm.inputNode.focus(); + } + }); + + // Toggle the timestamp on preference change + gDevTools.on("pref-changed", this._onToolboxPrefChanged); + this._onToolboxPrefChanged("pref-changed", { + pref: PREF_MESSAGE_TIMESTAMP, + newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP), + }); + + // focus input node + this.jsterm.inputNode.focus(); + }, + + /** + * Sets the focus to JavaScript input field when the web console tab is + * selected or when there is a split console present. + * @private + */ + _onPanelSelected: function WCF__onPanelSelected(evt, id) + { this.jsterm.inputNode.focus(); }, @@ -563,9 +626,9 @@ WebConsoleFrame.prototype = { */ _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs() { - let prefs = ["network", "networkinfo", "csserror", "cssparser", "exception", - "jswarn", "jslog", "error", "info", "warn", "log", "secerror", - "secwarn"]; + let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", + "exception", "jswarn", "jslog", "error", "info", "warn", "log", + "secerror", "secwarn", "netwarn", "netxhr"]; for (let pref of prefs) { this.filterPrefs[pref] = Services.prefs .getBoolPref(this._filterPrefsPrefix + pref); @@ -573,6 +636,28 @@ WebConsoleFrame.prototype = { }, /** + * Attach / detach reflow listeners depending on the checked status + * of the `CSS > Log` menuitem. + * + * @param function [aCallback=null] + * Optional function to invoke when the listener has been + * added/removed. + * + */ + _updateReflowActivityListener: + function WCF__updateReflowActivityListener(aCallback) + { + if (this.webConsoleClient) { + let pref = this._filterPrefsPrefix + "csslog"; + if (Services.prefs.getBoolPref(pref)) { + this.webConsoleClient.startListeners(["ReflowActivity"], aCallback); + } else { + this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback); + } + } + }, + + /** * Sets the events for the filter input field. * @private */ @@ -609,6 +694,9 @@ WebConsoleFrame.prototype = { let categories = this.document .querySelectorAll(".webconsole-filter-button[category]"); Array.forEach(categories, function(aButton) { + aButton.addEventListener("contextmenu", (aEvent) => { + aButton.open = true; + }, false); aButton.addEventListener("click", this._toggleFilter, false); let someChecked = false; @@ -623,6 +711,7 @@ WebConsoleFrame.prototype = { }, this); aButton.setAttribute("checked", someChecked); + aButton.setAttribute("aria-pressed", someChecked); }, this); if (!this.owner._browserConsole) { @@ -632,6 +721,15 @@ WebConsoleFrame.prototype = { let jslog = this.document.querySelector("menuitem[prefKey=jslog]"); jslog.hidden = true; } + + if (Services.appinfo.OS == "Darwin") { + let net = this.document.querySelector("toolbarbutton[category=net]"); + let accesskey = net.getAttribute("accesskeyMacOSX"); + net.setAttribute("accesskey", accesskey); + + let logging = this.document.querySelector("toolbarbutton[category=logging]"); + logging.removeAttribute("accesskey"); + } }, /** @@ -678,6 +776,34 @@ WebConsoleFrame.prototype = { this.outputNode.style.fontSize = ""; Services.prefs.clearUserPref("devtools.webconsole.fontSize"); } + this._updateCharSize(); + }, + + /** + * Calculates the width and height of a single character of the input box. + * This will be used in opening the popup at the correct offset. + * + * @private + */ + _updateCharSize: function WCF__updateCharSize() + { + let doc = this.document; + let tempLabel = doc.createElementNS(XHTML_NS, "span"); + let style = tempLabel.style; + style.position = "fixed"; + style.padding = "0"; + style.margin = "0"; + style.width = "auto"; + style.color = "transparent"; + WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel); + tempLabel.textContent = "x"; + doc.documentElement.appendChild(tempLabel); + this._inputCharWidth = tempLabel.offsetWidth; + tempLabel.parentNode.removeChild(tempLabel); + // Calculate the width of the chevron placed at the beginning of the input + // box. Remove 4 more pixels to accomodate the padding of the popup. + this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode) + .paddingLeft.replace(/[^0-9.]/g, "") - 4; }, /** @@ -692,7 +818,9 @@ WebConsoleFrame.prototype = { { let target = aEvent.target; let tagName = target.tagName; - if (tagName != aEvent.currentTarget.tagName) { + // Prevent toggle if generated from a contextmenu event (right click) + let isRightClick = (aEvent.button === 2); // right click is button 2; + if (tagName != aEvent.currentTarget.tagName || isRightClick) { return; } @@ -716,18 +844,38 @@ WebConsoleFrame.prototype = { break; } + // Toggle on the targeted filter button, and if the user alt clicked, + // toggle off all other filter buttons and their associated filters. let state = target.getAttribute("checked") !== "true"; + if (aEvent.getModifierState("Alt")) { + let buttons = this.document + .querySelectorAll(".webconsole-filter-button"); + Array.forEach(buttons, (button) => { + if (button !== target) { + button.setAttribute("checked", false); + button.setAttribute("aria-pressed", false); + this._setMenuState(button, false); + } + }); + state = true; + } target.setAttribute("checked", state); + target.setAttribute("aria-pressed", state); // This is a filter button with a drop-down, and the user clicked the // main part of the button. Go through all the severities and toggle // their associated filters. - let menuItems = target.querySelectorAll("menuitem"); - for (let i = 0; i < menuItems.length; i++) { - menuItems[i].setAttribute("checked", state); - let prefKey = menuItems[i].getAttribute("prefKey"); - this.setFilterState(prefKey, state); + this._setMenuState(target, state); + + // CSS reflow logging can decrease web page performance. + // Make sure the option is always unchecked when the CSS filter button is selected. + // See bug 971798. + if (target.getAttribute("category") == "css" && state) { + let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]"); + csslogMenuItem.setAttribute("checked", false); + this.setFilterState("csslog", false); } + break; } @@ -739,8 +887,9 @@ WebConsoleFrame.prototype = { this.setFilterState(prefKey, state); // Disable the log response and request body if network logging is off. - if (prefKey == "networkinfo" || prefKey == "network") { + if (prefKey == "networkinfo" || prefKey == "netxhr" || prefKey == "network") { let checkState = !this.getFilterState("networkinfo") && + !this.getFilterState("netxhr") && !this.getFilterState("network"); this.document.getElementById("saveBodies").disabled = checkState; this.document.getElementById("saveBodiesContextMenu").disabled = checkState; @@ -761,12 +910,32 @@ WebConsoleFrame.prototype = { } let toolbarButton = menuPopup.parentNode; toolbarButton.setAttribute("checked", someChecked); + toolbarButton.setAttribute("aria-pressed", someChecked); break; } } }, /** + * Set the menu attributes for a specific toggle button. + * + * @private + * @param XULElement aTarget + * Button with drop down items to be toggled. + * @param boolean aState + * True if the menu item is being toggled on, and false otherwise. + */ + _setMenuState: function WCF__setMenuState(aTarget, aState) + { + let menuItems = aTarget.querySelectorAll("menuitem"); + Array.forEach(menuItems, (item) => { + item.setAttribute("checked", aState); + let prefKey = item.getAttribute("prefKey"); + this.setFilterState(prefKey, aState); + }); + }, + + /** * Set the filter state for a specific toggle button. * * @param string aToggleType @@ -778,6 +947,7 @@ WebConsoleFrame.prototype = { this.filterPrefs[aToggleType] = aState; this.adjustVisibilityForMessageType(aToggleType, aState); Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState); + this._updateReflowActivityListener(); }, /** @@ -831,25 +1001,23 @@ WebConsoleFrame.prototype = { let outputNode = this.outputNode; let doc = this.document; - // Look for message nodes ("hud-msg-node") with the given preference key - // ("hud-msg-error", "hud-msg-cssparser", etc.) and add or remove the - // "hud-filtered-by-type" class, which turns on or off the display. + // Look for message nodes (".message") with the given preference key + // (filter="error", filter="cssparser", etc.) and add or remove the + // "filtered-by-type" class, which turns on or off the display. - let xpath = ".//*[contains(@class, 'hud-msg-node') and " + - "contains(concat(@class, ' '), 'hud-" + aPrefKey + " ')]"; + let xpath = ".//*[contains(@class, 'message') and " + + "@filter='" + aPrefKey + "']"; let result = doc.evaluate(xpath, outputNode, null, Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < result.snapshotLength; i++) { let node = result.snapshotItem(i); if (aState) { - node.classList.remove("hud-filtered-by-type"); + node.classList.remove("filtered-by-type"); } else { - node.classList.add("hud-filtered-by-type"); + node.classList.add("filtered-by-type"); } } - - this.regroupOutput(); }, /** @@ -859,25 +1027,23 @@ WebConsoleFrame.prototype = { adjustVisibilityOnSearchStringChange: function WCF_adjustVisibilityOnSearchStringChange() { - let nodes = this.outputNode.getElementsByClassName("hud-msg-node"); + let nodes = this.outputNode.getElementsByClassName("message"); let searchString = this.filterBox.value; for (let i = 0, n = nodes.length; i < n; ++i) { let node = nodes[i]; // hide nodes that match the strings - let text = node.clipboardText; + let text = node.textContent; // if the text matches the words in aSearchString... if (this.stringMatchesFilters(text, searchString)) { - node.classList.remove("hud-filtered-by-string"); + node.classList.remove("filtered-by-string"); } else { - node.classList.add("hud-filtered-by-string"); + node.classList.add("filtered-by-string"); } } - - this.regroupOutput(); }, /** @@ -897,7 +1063,7 @@ WebConsoleFrame.prototype = { let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity]; if (prefKey && !this.getFilterState(prefKey)) { // The node is filtered by type. - aNode.classList.add("hud-filtered-by-type"); + aNode.classList.add("filtered-by-type"); isFiltered = true; } @@ -907,11 +1073,11 @@ WebConsoleFrame.prototype = { // if string matches the filter text if (!this.stringMatchesFilters(text, search)) { - aNode.classList.add("hud-filtered-by-string"); + aNode.classList.add("filtered-by-string"); isFiltered = true; } - if (isFiltered && aNode.classList.contains("webconsole-msg-inspector")) { + if (isFiltered && aNode.classList.contains("inlined-variables-view")) { aNode.classList.add("hidden-message"); } @@ -930,15 +1096,17 @@ WebConsoleFrame.prototype = { mergeFilteredMessageNode: function WCF_mergeFilteredMessageNode(aOriginal, aFiltered) { - // childNodes[3].firstChild is the node containing the number of repetitions - // of a node. - let repeatNode = aOriginal.childNodes[3].firstChild; + let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0]; if (!repeatNode) { return; // no repeat node, return early. } let occurrences = parseInt(repeatNode.getAttribute("value")) + 1; repeatNode.setAttribute("value", occurrences); + repeatNode.textContent = occurrences; + let str = l10n.getStr("messageRepeats.tooltip2"); + repeatNode.title = PluralForm.get(occurrences, str) + .replace("#1", occurrences); }, /** @@ -953,7 +1121,7 @@ WebConsoleFrame.prototype = { */ _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode) { - let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0]; + let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; if (!repeatNode) { return null; } @@ -961,25 +1129,23 @@ WebConsoleFrame.prototype = { let uid = repeatNode._uid; let dupeNode = null; - if (aNode.classList.contains("webconsole-msg-cssparser") || - aNode.classList.contains("webconsole-msg-security")) { + if (aNode.category == CATEGORY_CSS || + aNode.category == CATEGORY_SECURITY) { dupeNode = this._repeatNodes[uid]; if (!dupeNode) { this._repeatNodes[uid] = aNode; } } - else if (!aNode.classList.contains("webconsole-msg-network") && - !aNode.classList.contains("webconsole-msg-inspector") && - (aNode.classList.contains("webconsole-msg-console") || - aNode.classList.contains("webconsole-msg-exception") || - aNode.classList.contains("webconsole-msg-error"))) { + else if ((aNode.category == CATEGORY_WEBDEV || + aNode.category == CATEGORY_JS) && + aNode.category != CATEGORY_NETWORK && + !aNode.classList.contains("inlined-variables-view")) { let lastMessage = this.outputNode.lastChild; if (!lastMessage) { return null; } - let lastRepeatNode = lastMessage - .getElementsByClassName("webconsole-msg-repeat")[0]; + let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0]; if (lastRepeatNode && lastRepeatNode._uid == uid) { dupeNode = lastMessage; } @@ -1044,6 +1210,7 @@ WebConsoleFrame.prototype = { let level = aMessage.level; let args = aMessage.arguments; let objectActors = new Set(); + let node = null; // Gather the actor IDs. args.forEach((aValue) => { @@ -1057,41 +1224,33 @@ WebConsoleFrame.prototype = { case "info": case "warn": case "error": - case "debug": + case "exception": + case "assert": + case "debug": { + let msg = new Messages.ConsoleGeneric(aMessage); + node = msg.init(this.output).render().element; + break; + } + case "table": { + let msg = new Messages.ConsoleTable(aMessage); + node = msg.init(this.output).render().element; + break; + } + case "trace": { + let msg = new Messages.ConsoleTrace(aMessage); + node = msg.init(this.output).render().element; + break; + } case "dir": { body = { arguments: args }; let clipboardArray = []; args.forEach((aValue) => { clipboardArray.push(VariablesView.getString(aValue)); - if (aValue && typeof aValue == "object" && - aValue.type == "longString") { - clipboardArray.push(l10n.getStr("longStringEllipsis")); - } }); clipboardText = clipboardArray.join(" "); break; } - case "trace": { - let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename); - let functionName = aMessage.functionName || - l10n.getStr("stacktrace.anonymousFunction"); - - body = l10n.getFormatStr("stacktrace.outputMessage", - [filename, functionName, sourceLine]); - - clipboardText = ""; - - aMessage.stacktrace.forEach(function(aFrame) { - clipboardText += aFrame.filename + " :: " + - aFrame.functionName + " :: " + - aFrame.lineNumber + "\n"; - }); - - clipboardText = clipboardText.trimRight(); - break; - } - case "group": case "groupCollapsed": clipboardText = body = aMessage.groupName; @@ -1129,6 +1288,20 @@ WebConsoleFrame.prototype = { break; } + case "count": { + let counter = aMessage.counter; + if (!counter) { + return null; + } + if (counter.error) { + Cu.reportError(l10n.getStr(counter.error)); + return null; + } + let msg = new Messages.ConsoleGeneric(aMessage); + node = msg.init(this.output).render().element; + break; + } + default: Cu.reportError("Unknown Console API log level: " + level); return null; @@ -1140,9 +1313,9 @@ WebConsoleFrame.prototype = { case "group": case "groupCollapsed": case "groupEnd": - case "trace": case "time": case "timeEnd": + case "count": for (let actor of objectActors) { this._releaseObject(actor); } @@ -1153,30 +1326,22 @@ WebConsoleFrame.prototype = { return null; // no need to continue } - let node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, - sourceURL, sourceLine, clipboardText, - level, aMessage.timeStamp); - if (aMessage.private) { - node.setAttribute("private", true); + if (!node) { + node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, + sourceURL, sourceLine, clipboardText, + level, aMessage.timeStamp); + if (aMessage.private) { + node.setAttribute("private", true); + } } if (objectActors.size > 0) { node._objectActors = objectActors; - let repeatNode = node.querySelector(".webconsole-msg-repeat"); - repeatNode._uid += [...objectActors].join("-"); - } - - // Make the node bring up the variables view, to allow the user to inspect - // the stack trace. - if (level == "trace") { - node._stacktrace = aMessage.stacktrace; - - this.makeOutputMessageLink(node, () => - this.jsterm.openVariablesView({ - rawObject: node._stacktrace, - autofocus: true, - })); + if (!node._messageObject) { + let repeatNode = node.getElementsByClassName("message-repeats")[0]; + repeatNode._uid += [...objectActors].join("-"); + } } return node; @@ -1195,26 +1360,6 @@ WebConsoleFrame.prototype = { }, /** - * The click event handler for objects shown inline coming from the - * window.console API. - * - * @private - * @param nsIDOMNode aAnchor - * The object inspector anchor element. This is the clickable element - * in the console.log message we display. - * @param object aObjectActor - * The object actor grip. - */ - _consoleLogClick: function WCF__consoleLogClick(aAnchor, aObjectActor) - { - this.jsterm.openVariablesView({ - label: aAnchor.textContent, - objectActor: aObjectActor, - autofocus: true, - }); - }, - - /** * Reports an error in the page source, either JavaScript or CSS. * * @param nsIScriptError aScriptError @@ -1226,9 +1371,19 @@ WebConsoleFrame.prototype = { { // Warnings and legacy strict errors become warnings; other types become // errors. - let severity = SEVERITY_ERROR; + let severity = 'error'; if (aScriptError.warning || aScriptError.strict) { - severity = SEVERITY_WARNING; + severity = 'warning'; + } + + let category = 'js'; + switch(aCategory) { + case CATEGORY_CSS: + category = 'css'; + break; + case CATEGORY_SECURITY: + category = 'security'; + break; } let objectActors = new Set(); @@ -1246,14 +1401,26 @@ WebConsoleFrame.prototype = { errorMessage = errorMessage.initial; } - let node = this.createMessageNode(aCategory, severity, - errorMessage, - aScriptError.sourceName, - aScriptError.lineNumber, null, null, - aScriptError.timeStamp); - if (aScriptError.private) { - node.setAttribute("private", true); - } + // Create a new message + let msg = new Messages.Simple(errorMessage, { + location: { + url: aScriptError.sourceName, + line: aScriptError.lineNumber, + column: aScriptError.columnNumber + }, + category: category, + severity: severity, + timestamp: aScriptError.timeStamp, + private: aScriptError.private, + filterDuplicates: true + }); + + let node = msg.init(this.output).render().element; + + // Select the body of the message node that is displayed in the console + let msgBody = node.getElementsByClassName("message-body")[0]; + // Add the more info link node to messages that belong to certain categories + this.addMoreInfoLink(msgBody, aScriptError); if (objectActors.size > 0) { node._objectActors = objectActors; @@ -1315,82 +1482,92 @@ WebConsoleFrame.prototype = { /** * Log network event. * - * @param object aActorId - * The network event actor ID to log. + * @param object aActor + * The network event actor to log. * @return nsIDOMElement|null * The message element to display in the Web Console output. */ - logNetEvent: function WCF_logNetEvent(aActorId) + logNetEvent: function WCF_logNetEvent(aActor) { - let networkInfo = this._networkRequests[aActorId]; + let actorId = aActor.actor; + let networkInfo = this._networkRequests[actorId]; if (!networkInfo) { return null; } let request = networkInfo.request; - - let msgNode = this.document.createElementNS(XUL_NS, "hbox"); - - let methodNode = this.document.createElementNS(XUL_NS, "label"); - methodNode.setAttribute("value", request.method); - methodNode.classList.add("webconsole-msg-body-piece"); - msgNode.appendChild(methodNode); - - let linkNode = this.document.createElementNS(XUL_NS, "hbox"); - linkNode.flex = 1; - linkNode.classList.add("webconsole-msg-body-piece"); - linkNode.classList.add("webconsole-msg-link"); - msgNode.appendChild(linkNode); - - let urlNode = this.document.createElementNS(XUL_NS, "label"); - urlNode.flex = 1; - urlNode.setAttribute("crop", "center"); - urlNode.setAttribute("title", request.url); - urlNode.setAttribute("tooltiptext", request.url); - urlNode.setAttribute("value", request.url); - urlNode.classList.add("hud-clickable"); - urlNode.classList.add("webconsole-msg-body-piece"); - urlNode.classList.add("webconsole-msg-url"); - linkNode.appendChild(urlNode); - + let clipboardText = request.method + " " + request.url; let severity = SEVERITY_LOG; + if (networkInfo.isXHR) { + clipboardText = request.method + " XHR " + request.url; + severity = SEVERITY_INFO; + } let mixedRequest = WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation); if (mixedRequest) { - urlNode.classList.add("webconsole-mixed-content"); - this.makeMixedContentNode(linkNode); - // If we define a SEVERITY_SECURITY in the future, switch this to - // SEVERITY_SECURITY. severity = SEVERITY_WARNING; } - let statusNode = this.document.createElementNS(XUL_NS, "label"); - statusNode.setAttribute("value", ""); - statusNode.classList.add("hud-clickable"); - statusNode.classList.add("webconsole-msg-body-piece"); - statusNode.classList.add("webconsole-msg-status"); - linkNode.appendChild(statusNode); - - let clipboardText = request.method + " " + request.url; + let methodNode = this.document.createElementNS(XHTML_NS, "span"); + methodNode.className = "method"; + methodNode.textContent = request.method + " "; let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, - msgNode, null, null, clipboardText); + methodNode, null, null, + clipboardText); if (networkInfo.private) { messageNode.setAttribute("private", true); } - - messageNode._connectionId = aActorId; + messageNode._connectionId = actorId; messageNode.url = request.url; - this.makeOutputMessageLink(messageNode, function WCF_net_message_link() { + let body = methodNode.parentNode; + body.setAttribute("aria-haspopup", true); + + if (networkInfo.isXHR) { + let xhrNode = this.document.createElementNS(XHTML_NS, "span"); + xhrNode.className = "xhr"; + xhrNode.textContent = l10n.getStr("webConsoleXhrIndicator"); + body.appendChild(xhrNode); + body.appendChild(this.document.createTextNode(" ")); + } + + let displayUrl = request.url; + let pos = displayUrl.indexOf("?"); + if (pos > -1) { + displayUrl = displayUrl.substr(0, pos); + } + + let urlNode = this.document.createElementNS(XHTML_NS, "a"); + urlNode.className = "url"; + urlNode.setAttribute("title", request.url); + urlNode.href = request.url; + urlNode.textContent = displayUrl; + urlNode.draggable = false; + body.appendChild(urlNode); + body.appendChild(this.document.createTextNode(" ")); + + if (mixedRequest) { + messageNode.classList.add("mixed-content"); + this.makeMixedContentNode(body); + } + + let statusNode = this.document.createElementNS(XHTML_NS, "a"); + statusNode.className = "status"; + body.appendChild(statusNode); + + let onClick = () => { if (!messageNode._panelOpen) { this.openNetworkPanel(messageNode, networkInfo); } - }.bind(this)); + }; + + this._addMessageLinkCallback(urlNode, onClick); + this._addMessageLinkCallback(statusNode, onClick); networkInfo.node = messageNode; - this._updateNetMessage(aActorId); + this._updateNetMessage(actorId); return messageNode; }, @@ -1406,19 +1583,85 @@ WebConsoleFrame.prototype = { let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; // Mixed content warning message links to a Learn More page - let mixedContentWarningNode = this.document.createElement("label"); - mixedContentWarningNode.setAttribute("value", mixedContentWarning); - mixedContentWarningNode.setAttribute("title", mixedContentWarning); - mixedContentWarningNode.classList.add("hud-clickable"); - mixedContentWarningNode.classList.add("webconsole-mixed-content-link"); + let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a"); + mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE; + mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE; + mixedContentWarningNode.className = "learn-more-link"; + mixedContentWarningNode.textContent = mixedContentWarning; + mixedContentWarningNode.draggable = false; aLinkNode.appendChild(mixedContentWarningNode); - mixedContentWarningNode.addEventListener("click", function(aEvent) { + this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => { + aEvent.stopPropagation(); this.owner.openLink(MIXED_CONTENT_LEARN_MORE); - aEvent.preventDefault(); + }); + }, + + /** + * Adds a more info link node to messages based on the nsIScriptError object + * that we need to report to the console + * + * @param aNode + * The node to which we will be adding the more info link node + * @param aScriptError + * The script error object that we are reporting to the console + */ + addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError) + { + let url; + switch (aScriptError.category) { + case "Insecure Password Field": + url = INSECURE_PASSWORDS_LEARN_MORE; + break; + case "Mixed Content Message": + case "Mixed Content Blocker": + url = MIXED_CONTENT_LEARN_MORE; + break; + case "Invalid HSTS Headers": + url = STRICT_TRANSPORT_SECURITY_LEARN_MORE; + break; + case "SHA-1 Signature": + url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE; + break; + default: + // Unknown category. Return without adding more info node. + return; + } + + this.addLearnMoreWarningNode(aNode, url); + }, + + /* + * Appends a clickable warning node to the node passed + * as a parameter to the function. When a user clicks on the appended + * warning node, the browser navigates to the provided url. + * + * @param aNode + * The node to which we will be adding a clickable warning node. + * @param aURL + * The url which points to the page where the user can learn more + * about security issues associated with the specific message that's + * being logged. + */ + addLearnMoreWarningNode: + function WCF_addLearnMoreWarningNode(aNode, aURL) + { + let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]"; + + let warningNode = this.document.createElementNS(XHTML_NS, "a"); + warningNode.title = aURL; + warningNode.href = aURL; + warningNode.draggable = false; + warningNode.textContent = moreInfoLabel; + warningNode.className = "learn-more-link"; + + this._addMessageLinkCallback(warningNode, (aEvent) => { aEvent.stopPropagation(); - }.bind(this)); + this.owner.openLink(aURL); + }); + + aNode.appendChild(warningNode); }, /** @@ -1431,21 +1674,19 @@ WebConsoleFrame.prototype = { */ logFileActivity: function WCF_logFileActivity(aFileURI) { - let urlNode = this.document.createElementNS(XUL_NS, "label"); - urlNode.flex = 1; - urlNode.setAttribute("crop", "center"); + let urlNode = this.document.createElementNS(XHTML_NS, "a"); urlNode.setAttribute("title", aFileURI); - urlNode.setAttribute("tooltiptext", aFileURI); - urlNode.setAttribute("value", aFileURI); - urlNode.classList.add("hud-clickable"); - urlNode.classList.add("webconsole-msg-url"); + urlNode.className = "url"; + urlNode.textContent = aFileURI; + urlNode.draggable = false; + urlNode.href = aFileURI; let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, urlNode, null, null, aFileURI); - this.makeOutputMessageLink(outputNode, function WCF__onFileClick() { + this._addMessageLinkCallback(urlNode, () => { this.owner.viewSource(aFileURI); - }.bind(this)); + }); return outputNode; }, @@ -1462,23 +1703,49 @@ WebConsoleFrame.prototype = { }, /** - * Inform user that the window.console API has been replaced by a script - * in a content page. + * Handle the reflow activity messages coming from the remote Web Console. + * + * @param object aMessage + * An object holding information about a reflow batch. */ - logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() + logReflowActivity: function WCF_logReflowActivity(aMessage) { - let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, - l10n.getStr("ConsoleAPIDisabled")); - this.outputMessage(CATEGORY_JS, node); + let {start, end, sourceURL, sourceLine} = aMessage; + let duration = Math.round((end - start) * 100) / 100; + let node = this.document.createElementNS(XHTML_NS, "span"); + if (sourceURL) { + node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]); + let a = this.document.createElementNS(XHTML_NS, "a"); + a.href = "#"; + a.draggable = "false"; + let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL); + let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction"); + a.textContent = l10n.getFormatStr("reflow.messageLinkText", + [functionName, filename, sourceLine]); + this._addMessageLinkCallback(a, () => { + this.owner.viewSourceInDebugger(sourceURL, sourceLine); + }); + node.appendChild(a); + } else { + node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]); + } + return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node); + }, + + + handleReflowActivity: function WCF_handleReflowActivity(aMessage) + { + this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]); }, /** - * Inform user that the string he tries to view is too long. + * Inform user that the window.console API has been replaced by a script + * in a content page. */ - logWarningAboutStringTooLong: function WCF_logWarningAboutStringTooLong() + logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() { let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, - l10n.getStr("longStringTooLong")); + l10n.getStr("ConsoleAPIDisabled")); this.outputMessage(CATEGORY_JS, node); }, @@ -1500,6 +1767,7 @@ WebConsoleFrame.prototype = { url: aActor.url, method: aActor.method, }, + isXHR: aActor.isXHR, response: {}, timings: {}, updates: [], // track the list of network event updates @@ -1507,7 +1775,7 @@ WebConsoleFrame.prototype = { }; this._networkRequests[aActor.actor] = networkInfo; - this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]); + this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor]); }, /** @@ -1557,8 +1825,12 @@ WebConsoleFrame.prototype = { break; } - if (networkInfo.node) { - this._updateNetMessage(aActorId); + if (networkInfo.node && this._updateNetMessage(aActorId)) { + this.emit("new-messages", new Set([{ + update: true, + node: networkInfo.node, + response: aPacket, + }])); } // For unit tests we pass the HTTP activity object to the test callback, @@ -1577,6 +1849,8 @@ WebConsoleFrame.prototype = { * @private * @param string aActorId * The network event actor ID for which you want to update the message. + * @return boolean + * |true| if the message node was updated, or |false| otherwise. */ _updateNetMessage: function WCF__updateNetMessage(aActorId) { @@ -1590,7 +1864,9 @@ WebConsoleFrame.prototype = { let hasEventTimings = updates.indexOf("eventTimings") > -1; let hasResponseStart = updates.indexOf("responseStart") > -1; let request = networkInfo.request; + let methodText = (networkInfo.isXHR)? request.method + ' XHR' : request.method; let response = networkInfo.response; + let updated = false; if (hasEventTimings || hasResponseStart) { let status = []; @@ -1603,22 +1879,25 @@ WebConsoleFrame.prototype = { } let statusText = "[" + status.join(" ") + "]"; - let linkNode = messageNode.querySelector(".webconsole-msg-link"); - let statusNode = linkNode.querySelector(".webconsole-msg-status"); - statusNode.setAttribute("value", statusText); + let statusNode = messageNode.getElementsByClassName("status")[0]; + statusNode.textContent = statusText; - messageNode.clipboardText = [request.method, request.url, statusText] + messageNode.clipboardText = [methodText, request.url, statusText] .join(" "); if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE && response.status <= MAX_HTTP_ERROR_CODE) { this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR); } + + updated = true; } if (messageNode._netPanel) { messageNode._netPanel.update(); } + + return updated; }, /** @@ -1637,7 +1916,7 @@ WebConsoleFrame.prototype = { let actor = aHttpActivity.actor; if (actor) { - this.webConsoleClient.getRequestHeaders(actor, function(aResponse) { + this.webConsoleClient.getRequestHeaders(actor, (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" + aResponse.error); @@ -1647,10 +1926,10 @@ WebConsoleFrame.prototype = { aHttpActivity.request.headers = aResponse.headers; this.webConsoleClient.getRequestCookies(actor, onRequestCookies); - }.bind(this)); + }); } - let onRequestCookies = function(aResponse) { + let onRequestCookies = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getRequestCookies:" + aResponse.error); @@ -1660,9 +1939,9 @@ WebConsoleFrame.prototype = { aHttpActivity.request.cookies = aResponse.cookies; this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders); - }.bind(this); + }; - let onResponseHeaders = function(aResponse) { + let onResponseHeaders = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" + aResponse.error); @@ -1672,9 +1951,9 @@ WebConsoleFrame.prototype = { aHttpActivity.response.headers = aResponse.headers; this.webConsoleClient.getResponseCookies(actor, onResponseCookies); - }.bind(this); + }; - let onResponseCookies = function(aResponse) { + let onResponseCookies = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getResponseCookies:" + aResponse.error); @@ -1684,9 +1963,9 @@ WebConsoleFrame.prototype = { aHttpActivity.response.cookies = aResponse.cookies; this.webConsoleClient.getRequestPostData(actor, onRequestPostData); - }.bind(this); + }; - let onRequestPostData = function(aResponse) { + let onRequestPostData = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getRequestPostData:" + aResponse.error); @@ -1697,9 +1976,9 @@ WebConsoleFrame.prototype = { aHttpActivity.discardRequestBody = aResponse.postDataDiscarded; this.webConsoleClient.getResponseContent(actor, onResponseContent); - }.bind(this); + }; - let onResponseContent = function(aResponse) { + let onResponseContent = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getResponseContent:" + aResponse.error); @@ -1710,9 +1989,9 @@ WebConsoleFrame.prototype = { aHttpActivity.discardResponseBody = aResponse.contentDiscarded; this.webConsoleClient.getEventTimings(actor, onEventTimings); - }.bind(this); + }; - let onEventTimings = function(aResponse) { + let onEventTimings = (aResponse) => { if (aResponse.error) { Cu.reportError("WCF_openNetworkPanel getEventTimings:" + aResponse.error); @@ -1722,9 +2001,9 @@ WebConsoleFrame.prototype = { aHttpActivity.timings = aResponse.timings; openPanel(); - }.bind(this); + }; - let openPanel = function() { + let openPanel = () => { aNode._netPanel = netPanel; let panel = netPanel.panel; @@ -1740,7 +2019,7 @@ WebConsoleFrame.prototype = { }); aNode._panelOpen = true; - }.bind(this); + }; let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this); netPanel.linkNode = aNode; @@ -1769,6 +2048,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, 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. * @@ -1784,7 +2092,9 @@ WebConsoleFrame.prototype = { * object and the arguments will be |aArguments|. * @param array [aArguments] * If a method is given to output the message element then the method - * will be invoked with the list of arguments given here. + * will be invoked with the list of arguments given here. The last + * object in this array should be the packet received from the + * back end. */ outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments) { @@ -1796,9 +2106,7 @@ WebConsoleFrame.prototype = { this._outputQueue.push([aCategory, aMethodOrNode, aArguments]); - if (!this._outputTimerInitialized) { - this._initOutputTimer(); - } + this._initOutputTimer(); }, /** @@ -1810,21 +2118,31 @@ WebConsoleFrame.prototype = { */ _flushMessageQueue: function WCF__flushMessageQueue() { + this._outputTimerInitialized = false; if (!this._outputTimer) { return; } - let timeSinceFlush = Date.now() - this._lastOutputFlush; - if (this._outputQueue.length > MESSAGES_IN_INTERVAL && - timeSinceFlush < THROTTLE_UPDATES) { - this._initOutputTimer(); - return; - } + let startTime = Date.now(); + let timeSinceFlush = startTime - this._lastOutputFlush; + let shouldThrottle = this._outputQueue.length > MESSAGES_IN_INTERVAL && + timeSinceFlush < THROTTLE_UPDATES; // Determine how many messages we can display now. let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL); - if (toDisplay < 1) { - this._outputTimerInitialized = false; + + // If there aren't any messages to display (because of throttling or an + // empty queue), then take care of some cleanup. Destroy items that were + // pruned from the outputQueue before being displayed. + if (shouldThrottle || toDisplay < 1) { + while (this._itemDestroyQueue.length) { + if ((Date.now() - startTime) > MAX_CLEANUP_TIME) { + break; + } + this._destroyItem(this._itemDestroyQueue.pop()); + } + + this._initOutputTimer(); return; } @@ -1836,30 +2154,29 @@ WebConsoleFrame.prototype = { } let batch = this._outputQueue.splice(0, toDisplay); - if (!batch.length) { - this._outputTimerInitialized = false; - return; - } - let outputNode = this.outputNode; let lastVisibleNode = null; - let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode); - let scrollBox = outputNode.scrollBoxObject.element; - + let scrollNode = outputNode.parentNode; let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId); + // We won't bother to try to restore scroll position if this is showing + // a lot of messages at once (and there are still items in the queue). + // It is going to purge whatever you were looking at anyway. + let scrolledToBottom = shouldPrune || + Utils.isOutputScrolledToBottom(outputNode); + // Output the current batch of messages. - let newMessages = new Set(); - let updatedMessages = new Set(); - for (let item of batch) { + let messages = new Set(); + for (let i = 0; i < batch.length; i++) { + let item = batch[i]; let result = this._outputMessageFromQueue(hudIdSupportsString, item); if (result) { - if (result.isRepeated) { - updatedMessages.add(result.isRepeated); - } - else { - newMessages.add(result.node); - } + messages.add({ + node: result.isRepeated ? result.isRepeated : result.node, + response: result.message, + update: !!result.isRepeated, + }); + if (result.visible && result.node == this.outputNode.lastChild) { lastVisibleNode = result.node; } @@ -1867,12 +2184,15 @@ WebConsoleFrame.prototype = { } let oldScrollHeight = 0; - - // Prune messages if needed. We do not do this for every flush call to - // improve performance. let removedNodes = 0; + + // Prune messages from the DOM, but only if needed. if (shouldPrune || !this._outputQueue.length) { - oldScrollHeight = scrollBox.scrollHeight; + // Only bother measuring the scrollHeight if not scrolled to bottom, + // since the oldScrollHeight will not be used if it is. + if (!scrolledToBottom) { + oldScrollHeight = scrollNode.scrollHeight; + } let categories = Object.keys(this._pruneCategoriesQueue); categories.forEach(function _pruneOutput(aCategory) { @@ -1881,14 +2201,9 @@ 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")); + (lastVisibleNode.category == CATEGORY_INPUT || + lastVisibleNode.category == CATEGORY_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 @@ -1897,28 +2212,25 @@ WebConsoleFrame.prototype = { Utils.scrollToVisible(lastVisibleNode); } else if (!scrolledToBottom && removedNodes > 0 && - oldScrollHeight != scrollBox.scrollHeight) { + oldScrollHeight != scrollNode.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; + scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight; } - if (newMessages.size) { - this.emit("messages-added", newMessages); - } - if (updatedMessages.size) { - this.emit("messages-updated", updatedMessages); + if (messages.size) { + this.emit("new-messages", messages); } - // If the queue is not empty, schedule another flush. - if (this._outputQueue.length > 0) { - this._initOutputTimer(); - } - else { - this._outputTimerInitialized = false; - this._flushCallback && this._flushCallback(); + // If the output queue is empty, then run _flushCallback. + if (this._outputQueue.length === 0 && this._flushCallback) { + if (this._flushCallback() === false) { + this._flushCallback = null; + } } + this._initOutputTimer(); + this._lastOutputFlush = Date.now(); }, @@ -1928,7 +2240,13 @@ WebConsoleFrame.prototype = { */ _initOutputTimer: function WCF__initOutputTimer() { - if (!this._outputTimer) { + let panelIsDestroyed = !this._outputTimer; + let alreadyScheduled = this._outputTimerInitialized; + let nothingToDo = !this._itemDestroyQueue.length && + !this._outputQueue.length; + + // Don't schedule a callback in the following cases: + if (panelIsDestroyed || alreadyScheduled || nothingToDo) { return; } @@ -1958,6 +2276,10 @@ WebConsoleFrame.prototype = { { let [category, methodOrNode, args] = aItem; + // The last object in the args array should be message + // object or response packet received from the server. + let message = (args && args.length) ? args[args.length-1] : null; + let node = typeof methodOrNode == "function" ? methodOrNode.apply(this, args || []) : methodOrNode; @@ -1995,6 +2317,7 @@ WebConsoleFrame.prototype = { visible: visible, node: node, isRepeated: isRepeated, + message: message }; }, @@ -2026,7 +2349,7 @@ WebConsoleFrame.prototype = { let n = Math.max(0, indexes.length - limit); pruned += n; for (let i = n - 1; i >= 0; i--) { - this._pruneItemFromQueue(this._outputQueue[indexes[i]]); + this._itemDestroyQueue.push(this._outputQueue[indexes[i]]); this._outputQueue.splice(indexes[i], 1); } } @@ -2036,14 +2359,18 @@ WebConsoleFrame.prototype = { }, /** - * Prune an item from the output queue. + * Destroy an item that was once in the outputQueue but isn't needed + * after all. * * @private * @param array aItem - * The item you want to remove from the output queue. + * The item you want to destroy. Does not remove it from the output + * queue. */ - _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem) + _destroyItem: function WCF__destroyItem(aItem) { + // TODO: handle object releasing in a more elegant way once all console + // messages use the new API - bug 778766. let [category, methodOrNode, args] = aItem; if (typeof methodOrNode != "function" && methodOrNode._objectActors) { for (let actor of methodOrNode._objectActors) { @@ -2052,10 +2379,23 @@ WebConsoleFrame.prototype = { methodOrNode._objectActors.clear(); } + if (methodOrNode == this.output._flushMessageQueue && + args[0]._objectActors) { + for (let arg of args) { + if (!arg._objectActors) { + continue; + } + for (let actor of arg._objectActors) { + this._releaseObject(actor); + } + arg._objectActors.clear(); + } + } + if (category == CATEGORY_NETWORK) { let connectionId = null; if (methodOrNode == this.logNetEvent) { - connectionId = args[0]; + connectionId = args[0].actor; } else if (typeof methodOrNode != "function") { connectionId = methodOrNode._connectionId; @@ -2102,15 +2442,11 @@ WebConsoleFrame.prototype = { */ pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory) { - let outputNode = this.outputNode; let logLimit = Utils.logLimitForCategory(aCategory); - - let messageNodes = outputNode.getElementsByClassName("webconsole-msg-" + - CATEGORY_CLASS_FRAGMENTS[aCategory]); + let messageNodes = this.outputNode.querySelectorAll(".message[category=" + + CATEGORY_CLASS_FRAGMENTS[aCategory] + "]"); let n = Math.max(0, messageNodes.length - logLimit); - let toRemove = Array.prototype.slice.call(messageNodes, 0, n); - toRemove.forEach(this.removeOutputMessage, this); - + [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this); return n; }, @@ -2122,6 +2458,10 @@ WebConsoleFrame.prototype = { */ removeOutputMessage: function WCF_removeOutputMessage(aNode) { + if (aNode._messageObject) { + aNode._messageObject.destroy(); + } + if (aNode._objectActors) { for (let actor of aNode._objectActors) { this._releaseObject(actor); @@ -2129,19 +2469,19 @@ WebConsoleFrame.prototype = { aNode._objectActors.clear(); } - if (aNode.classList.contains("webconsole-msg-cssparser") || - aNode.classList.contains("webconsole-msg-security")) { - let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0]; + if (aNode.category == CATEGORY_CSS || + aNode.category == CATEGORY_SECURITY) { + let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; if (repeatNode && repeatNode._uid) { delete this._repeatNodes[repeatNode._uid]; } } else if (aNode._connectionId && - aNode.classList.contains("webconsole-msg-network")) { + aNode.category == CATEGORY_NETWORK) { delete this._networkRequests[aNode._connectionId]; this._releaseObject(aNode._connectionId); } - else if (aNode.classList.contains("webconsole-msg-inspector")) { + else if (aNode.classList.contains("inlined-variables-view")) { let view = aNode._variablesView; if (view) { view.controller.releaseActors(); @@ -2149,32 +2489,7 @@ WebConsoleFrame.prototype = { aNode._variablesView = null; } - if (aNode.parentNode) { - aNode.parentNode.removeChild(aNode); - } - }, - - /** - * 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; - } + aNode.remove(); }, /** @@ -2202,8 +2517,8 @@ WebConsoleFrame.prototype = { * The timestamp to use for this message node. If omitted, the current * date and time is used. * @return nsIDOMNode - * The message node: a XUL richlistitem ready to be inserted into - * the Web Console output node. + * The message node: a DIV ready to be inserted into the Web Console + * output node. */ createMessageNode: function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL, @@ -2213,29 +2528,22 @@ WebConsoleFrame.prototype = { aClipboardText = aBody.innerText; } + let indentNode = this.document.createElementNS(XHTML_NS, "span"); + indentNode.className = "indent"; + + // Apply the current group by indenting appropriately. + let indent = this.groupDepth * GROUP_INDENT; + indentNode.style.width = indent + "px"; + // Make the icon container, which is a vertical box. Its purpose is to // ensure that the icon stays anchored at the top of the message even for // long multi-line messages. - let iconContainer = this.document.createElementNS(XUL_NS, "vbox"); - iconContainer.classList.add("webconsole-msg-icon-container"); - // Apply the curent group by indenting appropriately. - iconContainer.style.marginLeft = this.groupDepth * GROUP_INDENT + "px"; - - // Make the icon node. It's sprited and the actual region of the image is - // determined by CSS rules. - let iconNode = this.document.createElementNS(XUL_NS, "image"); - iconNode.classList.add("webconsole-msg-icon"); - iconContainer.appendChild(iconNode); - - // Make the spacer that positions the icon. - let spacer = this.document.createElementNS(XUL_NS, "spacer"); - spacer.flex = 1; - iconContainer.appendChild(spacer); + let iconContainer = this.document.createElementNS(XHTML_NS, "span"); + iconContainer.className = "icon"; // Create the message body, which contains the actual text of the message. - let bodyNode = this.document.createElementNS(XUL_NS, "description"); - bodyNode.flex = 1; - bodyNode.classList.add("webconsole-msg-body"); + let bodyNode = this.document.createElementNS(XHTML_NS, "span"); + bodyNode.className = "message-body-wrapper message-body devtools-monospace"; // Store the body text, since it is needed later for the variables view. let body = aBody; @@ -2245,8 +2553,15 @@ WebConsoleFrame.prototype = { (aBody + (aSourceURL ? " @ " + aSourceURL : "") + (aSourceLine ? ":" + aSourceLine : "")); + let timestamp = aTimeStamp || Date.now(); + // Create the containing node and append all its elements to it. - let node = this.document.createElementNS(XUL_NS, "richlistitem"); + let node = this.document.createElementNS(XHTML_NS, "div"); + node.id = "console-msg-" + gSequenceId(); + node.className = "message"; + node.clipboardText = aClipboardText; + node.timestamp = timestamp; + this.setMessageType(node, aCategory, aSeverity); if (aBody instanceof Ci.nsIDOMNode) { bodyNode.appendChild(aBody); @@ -2256,10 +2571,6 @@ WebConsoleFrame.prototype = { if (aLevel == "dir") { str = VariablesView.getString(aBody.arguments[0]); } - else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 && - typeof aBody == "object") { - this._makeConsoleLogMessageBody(node, bodyNode, aBody); - } else { str = aBody; } @@ -2270,48 +2581,47 @@ WebConsoleFrame.prototype = { } } - let repeatContainer = this.document.createElementNS(XUL_NS, "hbox"); - repeatContainer.setAttribute("align", "start"); - let repeatNode = this.document.createElementNS(XUL_NS, "label"); - repeatNode.setAttribute("value", "1"); - repeatNode.classList.add("webconsole-msg-repeat"); - repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel, - aSourceURL, aSourceLine].join(":"); - repeatContainer.appendChild(repeatNode); + // Add the message repeats node only when needed. + let repeatNode = null; + if (aCategory != CATEGORY_INPUT && + aCategory != CATEGORY_OUTPUT && + aCategory != CATEGORY_NETWORK && + !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) { + repeatNode = this.document.createElementNS(XHTML_NS, "span"); + repeatNode.setAttribute("value", "1"); + repeatNode.className = "message-repeats"; + repeatNode.textContent = 1; + repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel, + aSourceURL, aSourceLine].join(":"); + } // Create the timestamp. - let timestampNode = this.document.createElementNS(XUL_NS, "label"); - timestampNode.classList.add("webconsole-timestamp"); - let timestamp = aTimeStamp || Date.now(); + let timestampNode = this.document.createElementNS(XHTML_NS, "span"); + timestampNode.className = "timestamp devtools-monospace"; + let timestampString = l10n.timestampString(timestamp); - timestampNode.setAttribute("value", timestampString); + timestampNode.textContent = timestampString + " "; // Create the source location (e.g. www.example.com:6) that sits on the // right side of the message, if applicable. let locationNode; if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) { - locationNode = this.createLocationNode(aSourceURL, aSourceLine); + locationNode = this.createLocationNode({url: aSourceURL, + line: aSourceLine}); } - node.clipboardText = aClipboardText; - node.classList.add("hud-msg-node"); - - node.timestamp = timestamp; - this.setMessageType(node, aCategory, aSeverity); - node.appendChild(timestampNode); + node.appendChild(indentNode); node.appendChild(iconContainer); // Display the variables view after the message node. if (aLevel == "dir") { - let viewContainer = this.document.createElement("hbox"); - viewContainer.flex = 1; - viewContainer.height = this.outputNode.clientHeight * - CONSOLE_DIR_VIEW_HEIGHT; + bodyNode.style.height = (this.window.innerHeight * + CONSOLE_DIR_VIEW_HEIGHT) + "px"; let options = { objectActor: body.arguments[0], - targetElement: viewContainer, + targetElement: bodyNode, hideFilterInput: true, }; this.jsterm.openVariablesView(options).then((aView) => { @@ -2321,282 +2631,125 @@ WebConsoleFrame.prototype = { } }); - let bodyContainer = this.document.createElement("vbox"); - bodyContainer.flex = 1; - bodyContainer.appendChild(bodyNode); - bodyContainer.appendChild(viewContainer); - node.appendChild(bodyContainer); - node.classList.add("webconsole-msg-inspector"); + node.classList.add("inlined-variables-view"); } - else { - node.appendChild(bodyNode); + + node.appendChild(bodyNode); + if (repeatNode) { + node.appendChild(repeatNode); } - node.appendChild(repeatContainer); if (locationNode) { node.appendChild(locationNode); } - - node.setAttribute("id", "console-msg-" + gSequenceId()); + node.appendChild(this.document.createTextNode("\n")); return node; }, /** - * Make the message body for console.log() calls. - * - * @private - * @param nsIDOMElement aMessage - * The message element that holds the output for the given call. - * @param nsIDOMElement aContainer - * The specific element that will hold each part of the console.log - * output. - * @param object aBody - * The object given by this.logConsoleAPIMessage(). This object holds - * the call information that we need to display - mainly the arguments - * array of the given API call. - */ - _makeConsoleLogMessageBody: - function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody) - { - Object.defineProperty(aMessage, "_panelOpen", { - get: function() { - let nodes = aContainer.querySelectorAll(".hud-clickable"); - return Array.prototype.some.call(nodes, function(aNode) { - return aNode._panelOpen; - }); - }, - enumerable: true, - configurable: false - }); - - aBody.arguments.forEach(function(aItem) { - if (aContainer.firstChild) { - aContainer.appendChild(this.document.createTextNode(" ")); - } - - let text = VariablesView.getString(aItem); - let inspectable = !VariablesView.isPrimitive({ value: aItem }); - - if (aItem && typeof aItem != "object" || !inspectable) { - aContainer.appendChild(this.document.createTextNode(text)); - - if (aItem.type && aItem.type == "longString") { - let ellipsis = this.document.createElement("description"); - ellipsis.classList.add("hud-clickable"); - ellipsis.classList.add("longStringEllipsis"); - ellipsis.textContent = l10n.getStr("longStringEllipsis"); - - let formatter = function(s) '"' + s + '"'; - - this._addMessageLinkCallback(ellipsis, - this._longStringClick.bind(this, aMessage, aItem, formatter)); - - aContainer.appendChild(ellipsis); - } - return; - } - - // For inspectable objects. - let elem = this.document.createElement("description"); - elem.classList.add("hud-clickable"); - elem.setAttribute("aria-haspopup", "true"); - elem.appendChild(this.document.createTextNode(text)); - - this._addMessageLinkCallback(elem, - this._consoleLogClick.bind(this, elem, aItem)); - - aContainer.appendChild(elem); - }, this); - }, - - /** - * Click event handler for the ellipsis shown immediately after a long string. - * This method retrieves the full string and updates the console output to - * show it. - * - * @private - * @param nsIDOMElement aMessage - * The message element. - * @param object aActor - * The LongStringActor instance we work with. - * @param [function] aFormatter - * Optional function you can use to format the string received from the - * server, before being displayed in the console. - * @param nsIDOMElement aEllipsis - * The DOM element the user can click on to expand the string. - * @param nsIDOMEvent aEvent - * The DOM click event triggered by the user. - */ - _longStringClick: - function WCF__longStringClick(aMessage, aActor, aFormatter, aEllipsis, aEvent) - { - aEvent.preventDefault(); - - if (!aFormatter) { - aFormatter = function(s) s; - } - - let longString = this.webConsoleClient.longString(aActor); - let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH); - longString.substring(longString.initial.length, toIndex, - function WCF__onSubstring(aResponse) { - if (aResponse.error) { - Cu.reportError("WCF__longStringClick substring failure: " + - aResponse.error); - return; - } - - let node = aEllipsis.previousSibling; - node.textContent = aFormatter(longString.initial + aResponse.substring); - aEllipsis.parentNode.removeChild(aEllipsis); - - if (aMessage.category == CATEGORY_WEBDEV || - aMessage.category == CATEGORY_OUTPUT) { - aMessage.clipboardText = aMessage.textContent; - } - - this.emit("messages-updated", new Set([aMessage])); - - if (toIndex != longString.length) { - this.logWarningAboutStringTooLong(); - } - }.bind(this)); - }, - - /** - * Creates the XUL label that displays the textual location of an incoming + * Creates the anchor that displays the textual location of an incoming * message. * - * @param string aSourceURL - * The URL of the source file responsible for the error. - * @param number aSourceLine [optional] - * The line number on which the error occurred. If zero or omitted, - * there is no line number associated with this message. + * @param object aLocation + * An object containing url, line and column number of the message source (destructured). + * @param string aTarget [optional] + * Tells which tool to open the link with, on click. Supported tools: + * jsdebugger, styleeditor, scratchpad. * @return nsIDOMNode - * The new XUL label node, ready to be added to the message node. + * The new anchor element, ready to be added to the message node. */ - createLocationNode: function WCF_createLocationNode(aSourceURL, aSourceLine) + createLocationNode: + function WCF_createLocationNode({url, line, column}, aTarget) { - let locationNode = this.document.createElementNS(XUL_NS, "label"); + if (!url) { + url = ""; + } + let locationNode = this.document.createElementNS(XHTML_NS, "a"); + let filenameNode = this.document.createElementNS(XHTML_NS, "span"); // Create the text, which consists of an abbreviated version of the URL - // plus an optional line number. Scratchpad URLs should not be abbreviated. - let displayLocation; + // Scratchpad URLs should not be abbreviated. + let filename; let fullURL; + let isScratchpad = false; - if (/^Scratchpad\/\d+$/.test(aSourceURL)) { - displayLocation = aSourceURL; - fullURL = aSourceURL; + if (/^Scratchpad\/\d+$/.test(url)) { + filename = url; + fullURL = url; + isScratchpad = true; } else { - fullURL = aSourceURL.split(" -> ").pop(); - displayLocation = WebConsoleUtils.abbreviateSourceURL(fullURL); + fullURL = url.split(" -> ").pop(); + filename = WebConsoleUtils.abbreviateSourceURL(fullURL); } - if (aSourceLine) { - displayLocation += ":" + aSourceLine; - locationNode.sourceLine = aSourceLine; - } - - locationNode.setAttribute("value", displayLocation); + filenameNode.className = "filename"; + filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation")); + locationNode.appendChild(filenameNode); - // Style appropriately. - locationNode.setAttribute("crop", "center"); - locationNode.setAttribute("title", aSourceURL); - locationNode.setAttribute("tooltiptext", aSourceURL); - locationNode.classList.add("webconsole-location"); - locationNode.classList.add("text-link"); + locationNode.href = isScratchpad || !fullURL ? "#" : fullURL; + locationNode.draggable = false; + if (aTarget) { + locationNode.target = aTarget; + } + locationNode.setAttribute("title", url); + locationNode.className = "message-location theme-link devtools-monospace"; // Make the location clickable. - locationNode.addEventListener("click", () => { - if (/^Scratchpad\/\d+$/.test(aSourceURL)) { - let wins = Services.wm.getEnumerator("devtools:scratchpad"); - - while (wins.hasMoreElements()) { - let win = wins.getNext(); - - if (win.Scratchpad.uniqueName === aSourceURL) { - win.focus(); - return; - } - } + let onClick = () => { + let target = locationNode.target; + if (target == "scratchpad" || isScratchpad) { + this.owner.viewSourceInScratchpad(url); + return; } - else if (locationNode.parentNode.category == CATEGORY_CSS) { - this.owner.viewSourceInStyleEditor(fullURL, aSourceLine); + + let category = locationNode.parentNode.category; + if (target == "styleeditor" || category == CATEGORY_CSS) { + this.owner.viewSourceInStyleEditor(fullURL, line); } - else if (locationNode.parentNode.category == CATEGORY_JS || - locationNode.parentNode.category == CATEGORY_WEBDEV) { - this.owner.viewSourceInDebugger(fullURL, aSourceLine); + else if (target == "jsdebugger" || + category == CATEGORY_JS || category == CATEGORY_WEBDEV) { + this.owner.viewSourceInDebugger(fullURL, line); } else { - this.owner.viewSource(fullURL, aSourceLine); + this.owner.viewSource(fullURL, line); } - }, true); + }; + + if (fullURL) { + this._addMessageLinkCallback(locationNode, onClick); + } + + if (line) { + let lineNumberNode = this.document.createElementNS(XHTML_NS, "span"); + lineNumberNode.className = "line-number"; + lineNumberNode.textContent = ":" + line + (column >= 0 ? ":" + column : ""); + locationNode.appendChild(lineNumberNode); + locationNode.sourceLine = line; + } return locationNode; }, /** - * Adjusts the category and severity of the given message, clearing the old - * category and severity if present. + * Adjusts the category and severity of the given message. * * @param nsIDOMNode aMessageNode * The message node to alter. - * @param number aNewCategory - * The new category for the message; one of the CATEGORY_ constants. - * @param number aNewSeverity - * The new severity for the message; one of the SEVERITY_ constants. + * @param number aCategory + * The category for the message; one of the CATEGORY_ constants. + * @param number aSeverity + * The severity for the message; one of the SEVERITY_ constants. * @return void */ setMessageType: - function WCF_setMessageType(aMessageNode, aNewCategory, aNewSeverity) - { - // Remove the old CSS classes, if applicable. - if ("category" in aMessageNode) { - let oldCategory = aMessageNode.category; - let oldSeverity = aMessageNode.severity; - aMessageNode.classList.remove("webconsole-msg-" + - CATEGORY_CLASS_FRAGMENTS[oldCategory]); - aMessageNode.classList.remove("webconsole-msg-" + - SEVERITY_CLASS_FRAGMENTS[oldSeverity]); - let key = "hud-" + MESSAGE_PREFERENCE_KEYS[oldCategory][oldSeverity]; - aMessageNode.classList.remove(key); - } - - // Add in the new CSS classes. - aMessageNode.category = aNewCategory; - aMessageNode.severity = aNewSeverity; - aMessageNode.classList.add("webconsole-msg-" + - CATEGORY_CLASS_FRAGMENTS[aNewCategory]); - aMessageNode.classList.add("webconsole-msg-" + - SEVERITY_CLASS_FRAGMENTS[aNewSeverity]); - let key = "hud-" + MESSAGE_PREFERENCE_KEYS[aNewCategory][aNewSeverity]; - aMessageNode.classList.add(key); - }, - - /** - * Make a link given an output element. - * - * @param nsIDOMNode aNode - * The message element you want to make a link for. - * @param function aCallback - * The function you want invoked when the user clicks on the message - * element. - */ - makeOutputMessageLink: function WCF_makeOutputMessageLink(aNode, aCallback) + function WCF_setMessageType(aMessageNode, aCategory, aSeverity) { - let linkNode; - if (aNode.category === CATEGORY_NETWORK) { - linkNode = aNode.querySelector(".webconsole-msg-link, .webconsole-msg-url"); - } - else { - linkNode = aNode.querySelector(".webconsole-msg-body"); - linkNode.classList.add("hud-clickable"); - } - - linkNode.setAttribute("aria-haspopup", "true"); - - this._addMessageLinkCallback(aNode, aCallback); + aMessageNode.category = aCategory; + aMessageNode.severity = aSeverity; + aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]); + aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]); + aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]); }, /** @@ -2610,66 +2763,130 @@ WebConsoleFrame.prototype = { */ _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback) { - aNode.addEventListener("mousedown", function(aEvent) { + aNode.addEventListener("mousedown", (aEvent) => { + this._mousedown = true; + this._startX = aEvent.clientX; + this._startY = aEvent.clientY; + }, false); + + aNode.addEventListener("click", (aEvent) => { + let mousedown = this._mousedown; + this._mousedown = false; + + aEvent.preventDefault(); + + // Do not allow middle/right-click or 2+ clicks. + if (aEvent.detail != 1 || aEvent.button != 0) { + return; + } + + // If this event started with a mousedown event and it ends at a different + // location, we consider this text selection. + if (mousedown && + (this._startX != aEvent.clientX) && + (this._startY != aEvent.clientY)) + { + this._startX = this._startY = undefined; + return; + } + + this._startX = this._startY = undefined; + + aCallback.call(this, aEvent); + }, false); + }, + + _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback) + { + aNode.addEventListener("mousedown", (aEvent) => { + this._mousedown = true; this._startX = aEvent.clientX; this._startY = aEvent.clientY; }, false); - aNode.addEventListener("click", function(aEvent) { - if (aEvent.detail != 1 || aEvent.button != 0 || - (this._startX != aEvent.clientX && - this._startY != aEvent.clientY)) { + aNode.addEventListener("click", (aEvent) => { + let mousedown = this._mousedown; + this._mousedown = false; + + // Do not allow middle/right-click or 2+ clicks. + if (aEvent.detail != 1 || aEvent.button != 0) { + return; + } + + // If this event started with a mousedown event and it ends at a different + // location, we consider this text selection. + // Add a fuzz modifier of two pixels in any direction to account for sloppy + // clicking. + if (mousedown && + (Math.abs(aEvent.clientX - this._startX) >= 2) && + (Math.abs(aEvent.clientY - this._startY) >= 1)) + { + this._startX = this._startY = undefined; return; } - aCallback(this, aEvent); + this._startX = this._startY = undefined; + + aCallback.call(this, aEvent); }, false); }, /** + * Handler for the pref-changed event coming from the toolbox. + * Currently this function only handles the timestamps preferences. + * + * @private + * @param object aEvent + * This parameter is a string that holds the event name + * pref-changed in this case. + * @param object aData + * This is the pref-changed data object. + */ + _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData) + { + if (aData.pref == PREF_MESSAGE_TIMESTAMP) { + if (aData.newValue) { + this.outputNode.classList.remove("hideTimestamps"); + } + else { + this.outputNode.classList.add("hideTimestamps"); + } + } + }, + + /** * Copies the selected items to the system clipboard. * * @param object aOptions * - linkOnly: * An optional flag to copy only URL without timestamp and * other meta-information. Default is false. + * - contextmenu: + * An optional flag to copy the last clicked item which brought + * up the context menu if nothing is selected. Default is false. */ copySelectedItems: function WCF_copySelectedItems(aOptions) { - aOptions = aOptions || { linkOnly: false }; + aOptions = aOptions || { linkOnly: false, contextmenu: false }; // Gather up the selected items and concatenate their clipboard text. let strings = []; - let newGroup = false; - - let children = this.outputNode.children; - - for (let i = 0; i < children.length; i++) { - let item = children[i]; - if (!item.selected) { - 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; - } + let children = this.output.getSelectedMessages(); + if (!children.length && aOptions.contextmenu) { + children = [this._contextMenuHandler.lastClickedMessage]; + } + for (let item of children) { // 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")) { + if (!item.classList.contains("filtered-by-type") && + !item.classList.contains("filtered-by-string")) { let timestampString = l10n.timestampString(item.timestamp); - if (newGroup) { - strings.push("--"); - newGroup = false; - } - if (aOptions.linkOnly) { strings.push(item.url); } else { - strings.push("[" + timestampString + "] " + item.clipboardText); + strings.push(item.clipboardText); } } } @@ -2719,7 +2936,8 @@ WebConsoleFrame.prototype = { */ openSelectedItemInTab: function WCF_openSelectedItemInTab() { - let item = this.outputNode.selectedItem; + let item = this.output.getSelectedMessages(1)[0] || + this._contextMenuHandler.lastClickedMessage; if (!item || !item.url) { return; @@ -2744,8 +2962,18 @@ WebConsoleFrame.prototype = { this._destroyer = promise.defer(); + let toolbox = gDevTools.getToolbox(this.owner.target); + if (toolbox) { + toolbox.off("webconsole-selected", this._onPanelSelected); + } + + gDevTools.off("pref-changed", this._onToolboxPrefChanged); + this._repeatNodes = {}; + this._outputQueue.forEach(this._destroyItem, this); this._outputQueue = []; + this._itemDestroyQueue.forEach(this._destroyItem, this); + this._itemDestroyQueue = []; this._pruneCategoriesQueue = {}; this._networkRequests = {}; @@ -2754,17 +2982,23 @@ WebConsoleFrame.prototype = { this._outputTimer.cancel(); } this._outputTimer = null; - if (this.jsterm) { this.jsterm.destroy(); this.jsterm = null; } + this.output.destroy(); + this.output = null; + + if (this._contextMenuHandler) { + this._contextMenuHandler.destroy(); + this._contextMenuHandler = null; + } this._commandController = null; - let onDestroy = function() { + let onDestroy = () => { this._destroyer.resolve(null); - }.bind(this); + }; if (this.proxy) { this.proxy.disconnect().then(onDestroy); @@ -2837,6 +3071,7 @@ function JSTerm(aWebConsoleFrame) this._inputEventHandler = this._inputEventHandler.bind(this); this._focusEventHandler = this._focusEventHandler.bind(this); this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this); + this._blurEventHandler = this._blurEventHandler.bind(this); EventEmitter.decorate(this); } @@ -2851,6 +3086,29 @@ JSTerm.prototype = { lastCompletion: null, /** + * Array that caches the user input suggestions received from the server. + * @private + * @type array + */ + _autocompleteCache: null, + + /** + * The input that caused the last request to the server, whose response is + * cached in the _autocompleteCache array. + * @private + * @type string + */ + _autocompleteQuery: null, + + /** + * The frameActorId used in the last autocomplete query. Whenever this changes + * the autocomplete cache must be invalidated. + * @private + * @type string + */ + _lastFrameActorId: null, + + /** * The Web Console sidebar. * @see this._createSidebar() * @see Sidebar.jsm @@ -2929,34 +3187,51 @@ JSTerm.prototype = { COMPLETE_FORWARD: 0, COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, + COMPLETE_PAGEUP: 3, + COMPLETE_PAGEDOWN: 4, /** * Initialize the JSTerminal UI. */ init: function JST_init() { - let chromeDocument = this.hud.owner.chromeWindow.document; let autocompleteOptions = { onSelect: this.onAutocompleteSelect.bind(this), onClick: this.acceptProposedCompletion.bind(this), panelId: "webConsole_autocompletePopup", listBoxId: "webConsole_autocompletePopupListBox", position: "before_start", - theme: "light", + theme: "auto", direction: "ltr", autoSelect: true }; - this.autocompletePopup = new AutocompletePopup(chromeDocument, + this.autocompletePopup = new AutocompletePopup(this.hud.document, autocompleteOptions); let doc = this.hud.document; + let inputContainer = doc.querySelector(".jsterm-input-container"); this.completeNode = doc.querySelector(".jsterm-complete-node"); this.inputNode = doc.querySelector(".jsterm-input-node"); - this.inputNode.addEventListener("keypress", this._keyPress, false); - this.inputNode.addEventListener("input", this._inputEventHandler, false); - this.inputNode.addEventListener("keyup", this._inputEventHandler, false); - this.inputNode.addEventListener("focus", this._focusEventHandler, false); + if (this.hud.owner._browserConsole && + !Services.prefs.getBoolPref("devtools.chrome.enabled")) { + inputContainer.style.display = "none"; + } + else { + let okstring = l10n.getStr("selfxss.okstring"); + let msg = l10n.getFormatStr("selfxss.msg", [okstring]); + this._onPaste = WebConsoleUtils.pasteHandlerGen(this.inputNode, + doc.getElementById("webconsole-notificationbox"), + msg, okstring); + this.inputNode.addEventListener("keypress", this._keyPress, false); + this.inputNode.addEventListener("paste", this._onPaste); + this.inputNode.addEventListener("drop", this._onPaste); + this.inputNode.addEventListener("input", this._inputEventHandler, false); + this.inputNode.addEventListener("keyup", this._inputEventHandler, false); + this.inputNode.addEventListener("focus", this._focusEventHandler, false); + } + + this.hud.window.addEventListener("blur", this._blurEventHandler, false); this.lastInputValue && this.setInputValue(this.lastInputValue); }, @@ -2964,8 +3239,8 @@ JSTerm.prototype = { * The JavaScript evaluation response handler. * * @private - * @param nsIDOMElement [aAfterNode] - * Optional DOM element after which the evaluation result will be + * @param object [aAfterMessage] + * Optional message after which the evaluation result will be * inserted. * @param function [aCallback] * Optional function to invoke when the evaluation result is added to @@ -2974,7 +3249,7 @@ JSTerm.prototype = { * The message received from the server. */ _executeResultCallback: - function JST__executeResultCallback(aAfterNode, aCallback, aResponse) + function JST__executeResultCallback(aAfterMessage, aCallback, aResponse) { if (!this.hud) { return; @@ -2986,13 +3261,8 @@ JSTerm.prototype = { } let errorMessage = aResponse.exceptionMessage; let result = aResponse.result; - let inspectable = false; - if (result && !VariablesView.isPrimitive({ value: result })) { - inspectable = true; - } let helperResult = aResponse.helperResult; let helperHasRawOutput = !!(helperResult || {}).rawOutput; - let resultString = VariablesView.getString(result); if (helperResult && helperResult.type) { switch (helperResult.type) { @@ -3000,14 +3270,14 @@ JSTerm.prototype = { this.clearOutput(); break; case "inspectObject": - if (aAfterNode) { - if (!aAfterNode._objectActors) { - aAfterNode._objectActors = new Set(); + if (aAfterMessage) { + if (!aAfterMessage._objectActors) { + aAfterMessage._objectActors = new Set(); } - aAfterNode._objectActors.add(helperResult.object.actor); + aAfterMessage._objectActors.add(helperResult.object.actor); } this.openVariablesView({ - label: VariablesView.getString(helperResult.object), + label: VariablesView.getString(helperResult.object, { concise: true }), objectActor: helperResult.object, }); break; @@ -3022,6 +3292,9 @@ JSTerm.prototype = { case "help": this.hud.owner.openLink(HELP_URL); break; + case "copyValueToClipboard": + clipboardHelper.copyString(helperResult.value); + break; } } @@ -3033,60 +3306,32 @@ JSTerm.prototype = { return; } + let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage); + this.hud.output.addMessage(msg); + if (aCallback) { let oldFlushCallback = this.hud._flushCallback; - this.hud._flushCallback = function() { - aCallback(); - oldFlushCallback && oldFlushCallback(); - this.hud._flushCallback = oldFlushCallback; - }.bind(this); - } - - let node; + this.hud._flushCallback = () => { + aCallback(msg.element); + if (oldFlushCallback) { + oldFlushCallback(); + this.hud._flushCallback = oldFlushCallback; + return true; + } - if (errorMessage) { - node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, - aAfterNode, aResponse.timestamp); - } - else if (inspectable) { - node = this.writeOutputJS(resultString, - this._evalOutputClick.bind(this, aResponse), - aAfterNode, aResponse.timestamp); - } - else { - node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG, - aAfterNode, aResponse.timestamp); + return false; + }; } - node._objectActors = new Set(); + msg._afterMessage = aAfterMessage; + msg._objectActors = new Set(); - let error = aResponse.exception; - if (WebConsoleUtils.isActorGrip(error)) { - node._objectActors.add(error.actor); + if (WebConsoleUtils.isActorGrip(aResponse.exception)) { + msg._objectActors.add(aResponse.exception.actor); } if (WebConsoleUtils.isActorGrip(result)) { - node._objectActors.add(result.actor); - - if (result.type == "longString") { - // Add an ellipsis to expand the short string if the object is not - // inspectable. - - let body = node.querySelector(".webconsole-msg-body"); - let ellipsis = this.hud.document.createElement("description"); - ellipsis.classList.add("hud-clickable"); - ellipsis.classList.add("longStringEllipsis"); - ellipsis.textContent = l10n.getStr("longStringEllipsis"); - - let formatter = function(s) '"' + s + '"'; - let onclick = this.hud._longStringClick.bind(this.hud, node, result, - formatter); - this.hud._addMessageLinkCallback(ellipsis, onclick); - - body.appendChild(ellipsis); - - node.clipboardText += " " + ellipsis.textContent; - } + msg._objectActors.add(result.actor); } }, @@ -3098,19 +3343,44 @@ JSTerm.prototype = { * user input is used - taken from |this.inputNode.value|. * @param function [aCallback] * Optional function to invoke when the result is displayed. + * This is deprecated - please use the promise return value instead. + * @returns Promise + * Resolves with the message once the result is displayed. */ execute: function JST_execute(aExecuteString, aCallback) { + let deferred = promise.defer(); + let callback = function(msg) { + deferred.resolve(msg); + if (aCallback) { + aCallback(msg); + } + } + // attempt to execute the content of the inputNode aExecuteString = aExecuteString || this.inputNode.value; if (!aExecuteString) { return; } - let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); - let onResult = this._executeResultCallback.bind(this, node, aCallback); + let selectedNodeActor = null; + let inspectorSelection = this.hud.owner.getInspectorSelection(); + if (inspectorSelection) { + selectedNodeActor = inspectorSelection.nodeFront.actorID; + } + + let message = new Messages.Simple(aExecuteString, { + category: "input", + severity: "log", + }); + this.hud.output.addMessage(message); + let onResult = this._executeResultCallback.bind(this, message, callback); + + let options = { + frame: this.SELECTED_FRAME, + selectedNodeActor: selectedNodeActor, + }; - let options = { frame: this.SELECTED_FRAME }; this.requestEvaluation(aExecuteString, options).then(onResult, onResult); // Append a new value in the history of executed code, or overwrite the most @@ -3118,8 +3388,10 @@ JSTerm.prototype = { // value that was not evaluated yet. this.history[this.historyIndex++] = aExecuteString; this.historyPlaceHolder = this.history.length; + WebConsoleUtils.usageCount++; this.setInputValue(""); this.clearCompletion(); + return deferred.promise; }, /** @@ -3139,6 +3411,9 @@ JSTerm.prototype = { * user-selected stackframe. * If you do not provide a |frame| the string will be evaluated in the * global content window. + * - selectedNodeActor: tells the NodeActor ID of the current selection in + * the Inspector, if such a selection exists. This is used by helper + * functions that can evaluate on the current selection. * @return object * A promise object that is resolved when the server response is * received. @@ -3164,9 +3439,10 @@ JSTerm.prototype = { let evalOptions = { bindObjectActor: aOptions.bindObjectActor, frameActor: frameActor, + selectedNodeActor: aOptions.selectedNodeActor, }; - this.webConsoleClient.evaluateJS(aString, onResult, evalOptions); + this.webConsoleClient.evaluateJSAsync(aString, onResult, evalOptions); return deferred.promise; }, @@ -3249,14 +3525,16 @@ JSTerm.prototype = { let deferred = promise.defer(); openPromise = deferred.promise; let document = aOptions.targetElement.ownerDocument; - let iframe = document.createElement("iframe"); + let iframe = document.createElementNS(XHTML_NS, "iframe"); iframe.addEventListener("load", function onIframeLoad(aEvent) { iframe.removeEventListener("load", onIframeLoad, true); + iframe.style.visibility = "visible"; deferred.resolve(iframe.contentWindow); }, true); iframe.flex = 1; + iframe.style.visibility = "hidden"; iframe.setAttribute("src", VARIABLES_VIEW_URL); aOptions.targetElement.appendChild(iframe); } @@ -3279,7 +3557,6 @@ JSTerm.prototype = { _createSidebar: function JST__createSidebar() { let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); - let ToolSidebar = devtools.require("devtools/framework/sidebar").ToolSidebar; this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); this.sidebar.show(); }, @@ -3300,8 +3577,8 @@ JSTerm.prototype = { deferred.resolve(window); }; - let tab = this.sidebar.getTab("variablesview"); - if (tab) { + let tabPanel = this.sidebar.getTabPanel("variablesview"); + if (tabPanel) { if (this.sidebar.getCurrentTabID() == "variablesview") { onTabReady(); } @@ -3337,6 +3614,7 @@ JSTerm.prototype = { this._sidebarDestroy(); this.inputNode.focus(); + aEvent.stopPropagation(); }, /** @@ -3354,15 +3632,18 @@ JSTerm.prototype = { _createVariablesView: function JST__createVariablesView(aOptions) { let view = new VariablesView(aOptions.container); + view.toolbox = gDevTools.getToolbox(this.hud.owner.target); view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); view.emptyText = l10n.getStr("emptyPropertiesList"); view.searchEnabled = !aOptions.hideFilterInput; view.lazyEmpty = this._lazyVariablesView; - view.lazyAppend = this._lazyVariablesView; VariablesViewController.attach(view, { - getGripClient: aGrip => { - return new GripClient(this.hud.proxy.client, aGrip); + getEnvironmentClient: aGrip => { + return new EnvironmentClient(this.hud.proxy.client, aGrip); + }, + getObjectClient: aGrip => { + return new ObjectClient(this.hud.proxy.client, aGrip); }, getLongStringClient: aGrip => { return this.webConsoleClient.longString(aGrip); @@ -3398,7 +3679,6 @@ JSTerm.prototype = { _updateVariablesView: function JST__updateVariablesView(aOptions) { let view = aOptions.view; - view.createHierarchy(); view.empty(); // We need to avoid pruning the object inspection starting point. @@ -3407,7 +3687,9 @@ JSTerm.prototype = { return view._consoleLastObjectActor != aActor; }); - if (aOptions.objectActor) { + if (aOptions.objectActor && + (!this.hud.owner._browserConsole || + Services.prefs.getBoolPref("devtools.chrome.enabled"))) { // Make sure eval works in the correct context. view.eval = this._variablesViewEvaluate.bind(this, aOptions); view.switch = this._variablesViewSwitch.bind(this, aOptions); @@ -3419,20 +3701,13 @@ JSTerm.prototype = { view.delete = null; } - let scope = view.addScope(aOptions.label); - scope.expanded = true; - scope.locked = true; - - let container = scope.addItem(); - container.evaluationMacro = simpleValueEvalMacro; + let { variable, expanded } = view.controller.setSingleVariable(aOptions); + variable.evaluationMacro = simpleValueEvalMacro; if (aOptions.objectActor) { - view.controller.expand(container, aOptions.objectActor); view._consoleLastObjectActor = aOptions.objectActor.actor; } else if (aOptions.rawObject) { - container.populate(aOptions.rawObject); - view.commitHierarchy(); view._consoleLastObjectActor = null; } else { @@ -3440,7 +3715,9 @@ JSTerm.prototype = { "display."); } - this.emit("variablesview-updated", view, aOptions); + expanded.then(() => { + this.emit("variablesview-updated", view, aOptions); + }); }, /** @@ -3450,20 +3727,24 @@ JSTerm.prototype = { * @private * @param object aOptions * The options used for |this._updateVariablesView()|. - * @param string aString - * The string that the variables view wants to evaluate. + * @param object aVar + * The Variable object instance for the edited property. + * @param string aValue + * The value the edited property was changed to. */ - _variablesViewEvaluate: function JST__variablesViewEvaluate(aOptions, aString) + _variablesViewEvaluate: + function JST__variablesViewEvaluate(aOptions, aVar, aValue) { let updater = this._updateVariablesView.bind(this, aOptions); let onEval = this._silentEvalCallback.bind(this, updater); + let string = aVar.evaluationMacro(aVar, aValue); let evalOptions = { frame: this.SELECTED_FRAME, bindObjectActor: aOptions.objectActor.actor, }; - this.requestEvaluation(aString, evalOptions).then(onEval, onEval); + this.requestEvaluation(string, evalOptions).then(onEval, onEval); }, /** @@ -3547,14 +3828,16 @@ JSTerm.prototype = { return; } - let exception = aResponse.exception; - if (exception) { - let node = this.writeOutput(aResponse.exceptionMessage, - CATEGORY_OUTPUT, SEVERITY_ERROR, - null, aResponse.timestamp); - node._objectActors = new Set(); - if (WebConsoleUtils.isActorGrip(exception)) { - node._objectActors.add(exception.actor); + if (aResponse.exceptionMessage) { + let message = new Messages.Simple(aResponse.exceptionMessage, { + category: "output", + severity: "error", + timestamp: aResponse.timestamp, + }); + this.hud.output.addMessage(message); + message._objectActors = new Set(); + if (WebConsoleUtils.isActorGrip(aResponse.exception)) { + message._objectActors.add(aResponse.exception.actor); } } @@ -3575,68 +3858,6 @@ JSTerm.prototype = { }, - - /** - * Writes a JS object to the JSTerm outputNode. - * - * @param string aOutputMessage - * The message to display. - * @param function [aCallback] - * Optional function to invoke when users click the message. - * @param nsIDOMNode [aNodeAfter] - * Optional DOM node after which you want to insert the new message. - * This is used when execution results need to be inserted immediately - * after the user input. - * @param number [aTimestamp] - * Optional timestamp to show for the output message (millisconds since - * the UNIX epoch). If no timestamp is provided then Date.now() is - * used. - * @return nsIDOMNode - * The new message node. - */ - writeOutputJS: - function JST_writeOutputJS(aOutputMessage, aCallback, aNodeAfter, aTimestamp) - { - let node = this.writeOutput(aOutputMessage, CATEGORY_OUTPUT, SEVERITY_LOG, - aNodeAfter, aTimestamp); - if (aCallback) { - this.hud.makeOutputMessageLink(node, aCallback); - } - return node; - }, - - /** - * Writes a message to the HUD that originates from the interactive - * JavaScript console. - * - * @param string aOutputMessage - * The message to display. - * @param number aCategory - * The category of message: one of the CATEGORY_ constants. - * @param number aSeverity - * The severity of message: one of the SEVERITY_ constants. - * @param nsIDOMNode [aNodeAfter] - * Optional DOM node after which you want to insert the new message. - * This is used when execution results need to be inserted immediately - * after the user input. - * @param number [aTimestamp] - * Optional timestamp to show for the output message (millisconds since - * the UNIX epoch). If no timestamp is provided then Date.now() is - * used. - * @return nsIDOMNode - * The new message node. - */ - writeOutput: - function JST_writeOutput(aOutputMessage, aCategory, aSeverity, aNodeAfter, - aTimestamp) - { - let node = this.hud.createMessageNode(aCategory, aSeverity, aOutputMessage, - null, null, null, null, aTimestamp); - node._outputAfterNode = aNodeAfter; - this.hud.outputMessage(aCategory, node); - return node; - }, - /** * Clear the Web Console output. * @@ -3656,7 +3877,7 @@ JSTerm.prototype = { } hud.groupDepth = 0; - hud._outputQueue.forEach(hud._pruneItemFromQueue, hud); + hud._outputQueue.forEach(hud._destroyItem, hud); hud._outputQueue = []; hud._networkRequests = {}; hud._repeatNodes = {}; @@ -3675,7 +3896,7 @@ JSTerm.prototype = { */ clearPrivateMessages: function JST_clearPrivateMessages() { - let nodes = this.hud.outputNode.querySelectorAll("richlistitem[private]"); + let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); for (let node of nodes) { this.hud.removeOutputMessage(node); } @@ -3735,6 +3956,17 @@ JSTerm.prototype = { }, /** + * The window "blur" event handler. + * @private + */ + _blurEventHandler: function JST__blurEventHandler() + { + if (this.autocompletePopup) { + this.clearCompletion(); + } + }, + + /** * The inputNode "keypress" event handler. * * @private @@ -3747,30 +3979,6 @@ JSTerm.prototype = { if (aEvent.ctrlKey) { switch (aEvent.charCode) { - case 97: - // control-a - this.clearCompletion(); - - if (Services.appinfo.OS == "WINNT") { - // Allow Select All on Windows. - break; - } - - let lineBeginPos = 0; - if (this.hasMultilineInput()) { - // find index of closest newline <= to cursor - for (let i = inputNode.selectionStart-1; i >= 0; i--) { - if (inputNode.value.charAt(i) == "\r" || - inputNode.value.charAt(i) == "\n") { - lineBeginPos = i+1; - break; - } - } - } - inputNode.setSelectionRange(lineBeginPos, lineBeginPos); - aEvent.preventDefault(); - break; - case 101: // control-e if (Services.appinfo.OS == "WINNT") { @@ -3840,10 +4048,12 @@ JSTerm.prototype = { if (this.autocompletePopup.isOpen) { this.clearCompletion(); aEvent.preventDefault(); + aEvent.stopPropagation(); } else if (this.sidebar) { this._sidebarDestroy(); aEvent.preventDefault(); + aEvent.stopPropagation(); } break; @@ -3890,8 +4100,60 @@ JSTerm.prototype = { } break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEUP); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } + else { + this.hud.outputNode.parentNode.scrollTop = + Math.max(0, + this.hud.outputNode.parentNode.scrollTop - + this.hud.outputNode.parentNode.clientHeight + ); + } + aEvent.preventDefault(); + break; + + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } + else { + this.hud.outputNode.parentNode.scrollTop = + Math.min(this.hud.outputNode.parentNode.scrollHeight, + this.hud.outputNode.parentNode.scrollTop + + this.hud.outputNode.parentNode.clientHeight + ); + } + aEvent.preventDefault(); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_HOME: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = 0; + aEvent.preventDefault(); + } else if (this.inputNode.value.length <= 0) { + this.hud.outputNode.parentNode.scrollTop = 0; + aEvent.preventDefault(); + } + break; + case Ci.nsIDOMKeyEvent.DOM_VK_END: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = this.autocompletePopup.itemCount - 1; + aEvent.preventDefault(); + } else if (this.inputNode.value.length <= 0) { + this.hud.outputNode.parentNode.scrollTop = this.hud.outputNode.parentNode.scrollHeight; + aEvent.preventDefault(); + } + break; + case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: if (this.autocompletePopup.isOpen || this.lastCompletion.value) { this.clearCompletion(); @@ -4061,6 +4323,10 @@ JSTerm.prototype = { * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the * value stayed the same as the last time the function was called, * then the previous completion of all possible completions is used. + * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first + * item. + * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the + * last item. * - this.COMPLETE_HINT_ONLY: If there is more than one possible * completion and the input value stayed the same compared to the * last time this function was called, then the same completion is @@ -4077,21 +4343,26 @@ JSTerm.prototype = { { let inputNode = this.inputNode; let inputValue = inputNode.value; + let frameActor = this.getFrameActor(this.SELECTED_FRAME); + // If the inputNode has no value, then don't try to complete on it. if (!inputValue) { this.clearCompletion(); + aCallback && aCallback(this); + this.emit("autocomplete-updated"); return false; } - // Only complete if the selection is empty and at the end of the input. - if (inputNode.selectionStart == inputNode.selectionEnd && - inputNode.selectionEnd != inputValue.length) { + // Only complete if the selection is empty. + if (inputNode.selectionStart != inputNode.selectionEnd) { this.clearCompletion(); + aCallback && aCallback(this); + this.emit("autocomplete-updated"); return false; } // Update the completion results. - if (this.lastCompletion.value != inputValue) { + if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) { this._updateCompletionResult(aType, aCallback); return false; } @@ -4109,8 +4380,15 @@ JSTerm.prototype = { else if (aType == this.COMPLETE_FORWARD) { popup.selectNextItem(); } + else if (aType == this.COMPLETE_PAGEUP) { + popup.selectPreviousPageItem(); + } + else if (aType == this.COMPLETE_PAGEDOWN) { + popup.selectNextPageItem(); + } aCallback && aCallback(this); + this.emit("autocomplete-updated"); return accepted || popup.itemCount > 0; }, @@ -4127,16 +4405,54 @@ JSTerm.prototype = { _updateCompletionResult: function JST__updateCompletionResult(aType, aCallback) { - if (this.lastCompletion.value == this.inputNode.value) { + let frameActor = this.getFrameActor(this.SELECTED_FRAME); + if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) { return; } let requestId = gSequenceId(); - let input = this.inputNode.value; let cursor = this.inputNode.selectionStart; + let input = this.inputNode.value.substring(0, cursor); + let cache = this._autocompleteCache; + + // If the current input starts with the previous input, then we already + // have a list of suggestions and we just need to filter the cached + // suggestions. When the current input ends with a non-alphanumeric + // character we ask the server again for suggestions. + + // Check if last character is non-alphanumeric + if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) { + this._autocompleteQuery = null; + this._autocompleteCache = null; + } + + if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { + let filterBy = input; + // Find the last non-alphanumeric other than _ or $ if it exists. + let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/); + // If input contains non-alphanumerics, use the part after the last one + // to filter the cache + if (lastNonAlpha) { + filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); + } + + let newList = cache.sort().filter(function(l) { + return l.startsWith(filterBy); + }); + + this.lastCompletion = { + requestId: null, + completionType: aType, + value: null, + }; + + let response = { matches: newList, matchProp: filterBy }; + this._receiveAutocompleteProperties(null, aCallback, response); + return; + } + + this._lastFrameActorId = frameActor; - // TODO: Bug 787986 - throttle/disable updates, deal with slow/high latency - // network connections. this.lastCompletion = { requestId: requestId, completionType: aType, @@ -4145,7 +4461,8 @@ JSTerm.prototype = { let callback = this._receiveAutocompleteProperties.bind(this, requestId, aCallback); - this.webConsoleClient.autocomplete(input, cursor, callback); + + this.webConsoleClient.autocomplete(input, cursor, callback, frameActor); }, /** @@ -4170,11 +4487,21 @@ JSTerm.prototype = { aRequestId != this.lastCompletion.requestId) { return; } + // Cache whatever came from the server if the last char is alphanumeric or '.' + let cursor = inputNode.selectionStart; + let inputUntilCursor = inputValue.substring(0, cursor); + + if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { + this._autocompleteCache = aMessage.matches; + this._autocompleteQuery = inputUntilCursor; + } let matches = aMessage.matches; let lastPart = aMessage.matchProp; if (!matches.length) { this.clearCompletion(); + aCallback && aCallback(this); + this.emit("autocomplete-updated"); return; } @@ -4192,7 +4519,10 @@ JSTerm.prototype = { }; if (items.length > 1 && !popup.isOpen) { - popup.openPopup(inputNode); + let str = this.inputNode.value.substr(0, this.inputNode.selectionStart); + let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length; + let x = offset * this.hud._inputCharWidth; + popup.openPopup(inputNode, x + this.hud._chevronWidth); this._autocompletePopupNavigated = false; } else if (items.length < 2 && popup.isOpen) { @@ -4217,10 +4547,16 @@ JSTerm.prototype = { } aCallback && aCallback(this); + this.emit("autocomplete-updated"); }, onAutocompleteSelect: function JSTF_onAutocompleteSelect() { + // Render the suggestion only if the cursor is at the end of the input. + if (this.inputNode.selectionStart != this.inputNode.value.length) { + return; + } + let currentItem = this.autocompletePopup.selectedItem; if (currentItem && this.lastCompletion.value) { let suffix = currentItem.label.substring(this.lastCompletion. @@ -4262,7 +4598,11 @@ JSTerm.prototype = { if (currentItem && this.lastCompletion.value) { let suffix = currentItem.label.substring(this.lastCompletion. matchProp.length); - this.setInputValue(this.inputNode.value + suffix); + let cursor = this.inputNode.selectionStart; + let value = this.inputNode.value; + this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor)); + let newCursor = cursor + suffix.length; + this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; updated = true; } @@ -4284,21 +4624,6 @@ JSTerm.prototype = { this.completeNode.value = prefix + aSuffix; }, - /** - * The click event handler for evaluation results in the output. - * - * @private - * @param object aResponse - * The JavaScript evaluation response received from the server. - */ - _evalOutputClick: function JST__evalOutputClick(aResponse) - { - this.openVariablesView({ - label: VariablesView.getString(aResponse.result), - objectActor: aResponse.result, - autofocus: true, - }); - }, /** * Destroy the sidebar. @@ -4339,10 +4664,17 @@ JSTerm.prototype = { popup.parentNode.removeChild(popup); } + if (this._onPaste) { + this.inputNode.removeEventListener("paste", this._onPaste, false); + this.inputNode.removeEventListener("drop", this._onPaste, false); + this._onPaste = null; + } + this.inputNode.removeEventListener("keypress", this._keyPress, false); this.inputNode.removeEventListener("input", this._inputEventHandler, false); this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); this.inputNode.removeEventListener("focus", this._focusEventHandler, false); + this.hud.window.removeEventListener("blur", this._blurEventHandler, false); this.hud = null; }, @@ -4353,13 +4685,7 @@ JSTerm.prototype = { */ var Utils = { /** - * Flag to turn on and off scrolling. - */ - scroll: true, - - /** - * Scrolls a node so that it's visible in its containing XUL "scrollbox" - * element. + * Scrolls a node so that it's visible in its containing element. * * @param nsIDOMNode aNode * The node to make visible. @@ -4367,20 +4693,7 @@ var Utils = { */ scrollToVisible: function Utils_scrollToVisible(aNode) { - if (!this.scroll) { - return; - } - - // Find the enclosing richlistbox node. - let richListBoxNode = aNode.parentNode; - while (richListBoxNode.tagName != "richlistbox") { - richListBoxNode = richListBoxNode.parentNode; - } - - // Use the scroll box object interface to ensure the element is visible. - let boxObject = richListBoxNode.scrollBoxObject; - let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject); - nsIScrollBoxObject.ensureElementIsVisible(aNode); + aNode.scrollIntoView(false); }, /** @@ -4395,10 +4708,9 @@ var Utils = { { let lastNodeHeight = aOutputNode.lastChild ? aOutputNode.lastChild.clientHeight : 0; - let scrollBox = aOutputNode.scrollBoxObject.element; - - return scrollBox.scrollTop + scrollBox.clientHeight >= - scrollBox.scrollHeight - lastNodeHeight / 2; + let scrollNode = aOutputNode.parentNode; + return scrollNode.scrollTop + scrollNode.clientHeight >= + scrollNode.scrollHeight - lastNodeHeight / 2; }, /** @@ -4412,13 +4724,24 @@ var Utils = { */ categoryForScriptError: function Utils_categoryForScriptError(aScriptError) { - switch (aScriptError.category) { - case "CSS Parser": - case "CSS Loader": - return CATEGORY_CSS; + let category = aScriptError.category; + if (/^(?:CSS|Layout)\b/.test(category)) { + return CATEGORY_CSS; + } + + switch (category) { case "Mixed Content Blocker": + case "Mixed Content Message": case "CSP": + case "Invalid HSTS Headers": + case "Invalid HPKP Headers": + case "SHA-1 Signature": + case "Insecure Password Field": + case "SSL": + case "CORS": + case "Iframe Sandbox": + case "Tracking Protection": return CATEGORY_SECURITY; default: @@ -4465,20 +4788,11 @@ function CommandController(aWebConsole) CommandController.prototype = { /** - * Copies the currently-selected entries in the Web Console output to the - * clipboard. - */ - copy: function CommandController_copy() - { - this.owner.copySelectedItems(); - }, - - /** * Selects all the text in the HUD output. */ selectAll: function CommandController_selectAll() { - this.owner.outputNode.selectAll(); + this.owner.output.selectAllMessages(); }, /** @@ -4491,33 +4805,48 @@ CommandController.prototype = { copyURL: function CommandController_copyURL() { - this.owner.copySelectedItems({ linkOnly: true }); + this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); + }, + + /** + * Copies the last clicked message. + */ + copyLastClicked: function CommandController_copy() + { + this.owner.copySelectedItems({ linkOnly: false, contextmenu: true }); }, supportsCommand: function CommandController_supportsCommand(aCommand) { + if (!this.owner || !this.owner.output) { + return false; + } return this.isCommandEnabled(aCommand); }, isCommandEnabled: function CommandController_isCommandEnabled(aCommand) { switch (aCommand) { - case "cmd_copy": - // Only enable "copy" if nodes are selected. - return this.owner.outputNode.selectedCount > 0; case "consoleCmd_openURL": case "consoleCmd_copyURL": { // Only enable URL-related actions if node is Net Activity. - let selectedItem = this.owner.outputNode.selectedItem; + let selectedItem = this.owner.output.getSelectedMessages(1)[0] || + this.owner._contextMenuHandler.lastClickedMessage; return selectedItem && "url" in selectedItem; } + case "cmd_copy": { + // Only copy if we right-clicked the console and there's no selected text. + // With text selected, we want to fall back onto the default copy behavior. + return this.owner._contextMenuHandler.lastClickedMessage && + !this.owner.output.getSelectedMessages(1)[0]; + } case "consoleCmd_clearOutput": - case "cmd_fontSizeEnlarge": - case "cmd_fontSizeReduce": - case "cmd_fontSizeReset": case "cmd_selectAll": case "cmd_find": return true; + case "cmd_fontSizeEnlarge": + case "cmd_fontSizeReduce": + case "cmd_fontSizeReset": case "cmd_close": return this.owner.owner._browserConsole; } @@ -4527,9 +4856,6 @@ CommandController.prototype = { doCommand: function CommandController_doCommand(aCommand) { switch (aCommand) { - case "cmd_copy": - this.copy(); - break; case "consoleCmd_openURL": this.openURL(); break; @@ -4539,6 +4865,9 @@ CommandController.prototype = { case "consoleCmd_clearOutput": this.owner.jsterm.clearOutput(true); break; + case "cmd_copy": + this.copyLastClicked(); + break; case "cmd_find": this.owner.filterBox.focus(); break; @@ -4586,6 +4915,7 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget) this._onNetworkEvent = this._onNetworkEvent.bind(this); this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); this._onFileActivity = this._onFileActivity.bind(this); + this._onReflowActivity = this._onReflowActivity.bind(this); this._onTabNavigated = this._onTabNavigated.bind(this); this._onAttachConsole = this._onAttachConsole.bind(this); this._onCachedMessages = this._onCachedMessages.bind(this); @@ -4677,12 +5007,12 @@ WebConsoleConnectionProxy.prototype = { timeout, Ci.nsITimer.TYPE_ONE_SHOT); let connPromise = this._connectDefer.promise; - connPromise.then(function _onSucess() { + connPromise.then(() => { this._connectTimer.cancel(); this._connectTimer = null; - }.bind(this), function _onFailure() { + }, () => { this._connectTimer = null; - }.bind(this)); + }); let client = this.client = this.target.client; @@ -4692,6 +5022,7 @@ WebConsoleConnectionProxy.prototype = { client.addListener("networkEvent", this._onNetworkEvent); client.addListener("networkEventUpdate", this._onNetworkEventUpdate); client.addListener("fileActivity", this._onFileActivity); + client.addListener("reflowActivity", this._onReflowActivity); client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited); this.target.on("will-navigate", this._onTabNavigated); this.target.on("navigate", this._onTabNavigated); @@ -4757,6 +5088,8 @@ WebConsoleConnectionProxy.prototype = { let msgs = ["PageError", "ConsoleAPI"]; this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); + + this.owner._updateReflowActivityListener(); }, /** @@ -4894,6 +5227,13 @@ WebConsoleConnectionProxy.prototype = { } }, + _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handleReflowActivity(aPacket); + } + }, + /** * The "lastPrivateContextExited" message type handler. When this message is * received the Web Console UI is cleared. @@ -4928,17 +5268,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); }, /** @@ -4979,6 +5309,7 @@ WebConsoleConnectionProxy.prototype = { this.client.removeListener("networkEvent", this._onNetworkEvent); this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate); this.client.removeListener("fileActivity", this._onFileActivity); + this.client.removeListener("reflowActivity", this._onReflowActivity); this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited); this.target.off("will-navigate", this._onTabNavigated); this.target.off("navigate", this._onTabNavigated); @@ -5000,41 +5331,35 @@ function gSequenceId() } gSequenceId.n = 0; - -function goUpdateConsoleCommands() { - goUpdateCommand("consoleCmd_openURL"); - goUpdateCommand("consoleCmd_copyURL"); -} - - - /////////////////////////////////////////////////////////////////////////////// // Context Menu /////////////////////////////////////////////////////////////////////////////// -const CONTEXTMENU_ID = "output-contextmenu"; - /* - * ConsoleContextMenu: This handle to show/hide a context menu item. + * ConsoleContextMenu this used to handle the visibility of context menu items. + * + * @constructor + * @param object aOwner + * The WebConsoleFrame instance that owns this object. */ -let ConsoleContextMenu = { +function ConsoleContextMenu(aOwner) +{ + this.owner = aOwner; + this.popup = this.owner.document.getElementById("output-contextmenu"); + this.build = this.build.bind(this); + this.popup.addEventListener("popupshowing", this.build); +} + +ConsoleContextMenu.prototype = { + lastClickedMessage: null, + /* * Handle to show/hide context menu item. - * - * @param nsIDOMEvent aEvent */ build: function CCM_build(aEvent) { - let popup = aEvent.target; - if (popup.id !== CONTEXTMENU_ID) { - return; - } - - let view = document.querySelector(".hud-output-node"); - let metadata = this.getSelectionMetadata(view); - - for (let i = 0, l = popup.childNodes.length; i < l; ++i) { - let element = popup.childNodes[i]; + let metadata = this.getSelectionMetadata(aEvent.rangeParent); + for (let element of this.popup.children) { element.hidden = this.shouldHideMenuItem(element, metadata); } }, @@ -5042,21 +5367,27 @@ let ConsoleContextMenu = { /* * Get selection information from the view. * - * @param nsIDOMElement aView - * This should be <xul:richlistbox>. - * + * @param nsIDOMElement aClickElement + * The DOM element the user clicked on. * @return object * Selection metadata. */ - getSelectionMetadata: function CCM_getSelectionMetadata(aView) + getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement) { let metadata = { selectionType: "", selection: new Set(), }; - let selectedItems = aView.selectedItems; + let selectedItems = this.owner.output.getSelectedMessages(); + if (!selectedItems.length) { + let clickedItem = this.owner.output.getMessageForElement(aClickElement); + if (clickedItem) { + this.lastClickedMessage = clickedItem; + selectedItems = [clickedItem]; + } + } - metadata.selectionType = (selectedItems > 1) ? "multiple" : "single"; + metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; let selection = metadata.selection; for (let item of selectedItems) { @@ -5111,4 +5442,15 @@ let ConsoleContextMenu = { return shouldHide; }, + + /** + * Destroy the ConsoleContextMenu object instance. + */ + destroy: function CCM_destroy() + { + this.popup.removeEventListener("popupshowing", this.build); + this.popup = null; + this.owner = null; + this.lastClickedMessage = null; + }, }; diff --git a/browser/devtools/webconsole/webconsole.xul b/browser/devtools/webconsole/webconsole.xul index b4e07aa49..2d5d85f6b 100644 --- a/browser/devtools/webconsole/webconsole.xul +++ b/browser/devtools/webconsole/webconsole.xul @@ -9,6 +9,8 @@ <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" + type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/devtools/webconsole.css" type="text/css"?> <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> @@ -21,14 +23,22 @@ windowtype="devtools:webconsole" width="900" height="350" persist="screenX screenY width height sizemode"> + + <script type="application/javascript;version=1.8" + src="chrome://browser/content/devtools/theme-switching.js"/> <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/> - <script type="text/javascript" src="webconsole.js"/> + <script type="text/javascript"><![CDATA[ +function goUpdateConsoleCommands() { + goUpdateCommand("consoleCmd_openURL"); + goUpdateCommand("consoleCmd_copyURL"); +} + // ]]></script> <commandset id="editMenuCommands"/> <commandset id="consoleCommands" commandupdater="true" - events="richlistbox-select" + events="focus,select" oncommandupdate="goUpdateConsoleCommands();"> <command id="consoleCmd_openURL" oncommand="goDoCommand('consoleCmd_openURL');"/> @@ -37,28 +47,27 @@ <command id="consoleCmd_clearOutput" oncommand="goDoCommand('consoleCmd_clearOutput');"/> <command id="cmd_find" oncommand="goDoCommand('cmd_find');"/> - <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');"/> - <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');"/> - <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');"/> + <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');" disabled="true"/> + <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');" disabled="true"/> + <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');" disabled="true"/> <command id="cmd_close" oncommand="goDoCommand('cmd_close');" disabled="true"/> </commandset> <keyset id="consoleKeys"> - <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> - <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/> <key key="&findCmd.key;" command="cmd_find" modifiers="accel"/> - <key key="&clearOutputCmd.key;" command="consoleCmd_clearOutput" modifiers="accel"/> <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/> + <key key="&clearOutputCtrl.key;" command="consoleCmd_clearOutput" modifiers="control"/> </keyset> <keyset id="editMenuKeys"/> <popupset id="mainPopupSet"> - <menupopup id="output-contextmenu" - onpopupshowing="ConsoleContextMenu.build(event);"> + <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()"> <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;" accesskey="&saveBodies.accesskey;"/> <menuitem id="menu_openURL" label="&openURL.label;" @@ -72,83 +81,89 @@ </menupopup> </popupset> - <box class="hud-outer-wrapper devtools-responsive-container" flex="1"> - <vbox class="hud-console-wrapper" flex="1"> - <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full"> - <toolbarbutton label="&btnPageNet.label;" type="menu-button" - category="net" class="devtools-toolbarbutton webconsole-filter-button" - tooltiptext="&btnPageNet.tooltip;" -#ifdef XP_MACOSX - accesskey="&btnPageNet.accesskeyMacOSX;" -#else - accesskey="&btnPageNet.accesskey;" -#endif - tabindex="3"> - <menupopup> - <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false" - prefKey="network"/> - <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" - prefKey="networkinfo"/> - <menuseparator id="saveBodiesSeparator" /> - <menuitem id="saveBodies" type="checkbox" label="&saveBodies.label;" - accesskey="&saveBodies.accesskey;"/> - </menupopup> - </toolbarbutton> - <toolbarbutton label="&btnPageCSS.label;" type="menu-button" - category="css" class="devtools-toolbarbutton webconsole-filter-button" - tooltiptext="&btnPageCSS.tooltip;" - accesskey="&btnPageCSS.accesskey;" - tabindex="4"> - <menupopup> - <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false" - prefKey="csserror"/> - <menuitem label="&btnConsoleWarnings;" type="checkbox" - autocheck="false" prefKey="cssparser"/> - </menupopup> - </toolbarbutton> - <toolbarbutton label="&btnPageJS.label;" type="menu-button" - category="js" class="devtools-toolbarbutton webconsole-filter-button" - tooltiptext="&btnPageJS.tooltip;" - accesskey="&btnPageJS.accesskey;" - tabindex="5"> - <menupopup> - <menuitem label="&btnConsoleErrors;" type="checkbox" - autocheck="false" prefKey="exception"/> - <menuitem label="&btnConsoleWarnings;" type="checkbox" - autocheck="false" prefKey="jswarn"/> - <menuitem label="&btnConsoleLog;" type="checkbox" - autocheck="false" prefKey="jslog"/> - </menupopup> - </toolbarbutton> - <toolbarbutton label="&btnPageSecurity.label;" type="menu-button" - category="security" class="devtools-toolbarbutton webconsole-filter-button" - tooltiptext="&btnPageSecurity.tooltip;" - accesskey="&btnPageSecurity.accesskey;" - tabindex="6"> - <menupopup> - <menuitem label="&btnConsoleErrors;" type="checkbox" - autocheck="false" prefKey="secerror"/> - <menuitem label="&btnConsoleWarnings;" type="checkbox" - autocheck="false" prefKey="secwarn"/> - </menupopup> - </toolbarbutton> - <toolbarbutton label="&btnPageLogging.label;" type="menu-button" - category="logging" class="devtools-toolbarbutton webconsole-filter-button" - tooltiptext="&btnPageLogging.tooltip;" - accesskey="&btnPageLogging.accesskey;" - tabindex="7"> - <menupopup> - <menuitem label="&btnConsoleErrors;" type="checkbox" - autocheck="false" prefKey="error"/> - <menuitem label="&btnConsoleWarnings;" type="checkbox" - autocheck="false" prefKey="warn"/> - <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false" - prefKey="info"/> - <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" - prefKey="log"/> - </menupopup> - </toolbarbutton> + <tooltip id="aHTMLTooltip" page="true"/> + <box class="hud-outer-wrapper devtools-responsive-container theme-body" flex="1"> + <vbox class="hud-console-wrapper devtools-main-content" flex="1"> + <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full"> + <hbox class="devtools-toolbarbutton-group"> + <toolbarbutton label="&btnPageNet.label;" type="menu-button" + category="net" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageNet.tooltip;" + accesskeyMacOSX="&btnPageNet.accesskeyMacOSX;" + accesskey="&btnPageNet.accesskey;" + tabindex="3"> + <menupopup id="net-contextmenu"> + <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false" + prefKey="network"/> + <menuitem label="&btnConsoleWarnings;" type="checkbox" autocheck="false" + prefKey="netwarn"/> + <menuitem label="&btnConsoleXhr;" type="checkbox" autocheck="false" + prefKey="netxhr"/> + <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" + prefKey="networkinfo"/> + <menuseparator id="saveBodiesSeparator" /> + <menuitem id="saveBodies" type="checkbox" label="&saveBodies.label;" + accesskey="&saveBodies.accesskey;"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnPageCSS.label;" type="menu-button" + category="css" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageCSS.tooltip2;" + accesskey="&btnPageCSS.accesskey;" + tabindex="4"> + <menupopup id="css-contextmenu"> + <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false" + prefKey="csserror"/> + <menuitem label="&btnConsoleWarnings;" type="checkbox" + autocheck="false" prefKey="cssparser"/> + <menuitem label="&btnConsoleReflows;" type="checkbox" + autocheck="false" prefKey="csslog"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnPageJS.label;" type="menu-button" + category="js" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageJS.tooltip;" + accesskey="&btnPageJS.accesskey;" + tabindex="5"> + <menupopup id="js-contextmenu"> + <menuitem label="&btnConsoleErrors;" type="checkbox" + autocheck="false" prefKey="exception"/> + <menuitem label="&btnConsoleWarnings;" type="checkbox" + autocheck="false" prefKey="jswarn"/> + <menuitem label="&btnConsoleLog;" type="checkbox" + autocheck="false" prefKey="jslog"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnPageSecurity.label;" type="menu-button" + category="security" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageSecurity.tooltip;" + accesskey="&btnPageSecurity.accesskey;" + tabindex="6"> + <menupopup id="security-contextmenu"> + <menuitem label="&btnConsoleErrors;" type="checkbox" + autocheck="false" prefKey="secerror"/> + <menuitem label="&btnConsoleWarnings;" type="checkbox" + autocheck="false" prefKey="secwarn"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnPageLogging.label;" type="menu-button" + category="logging" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageLogging.tooltip;" + accesskey="&btnPageLogging.accesskey3;" + tabindex="7"> + <menupopup id="logging-contextmenu"> + <menuitem label="&btnConsoleErrors;" type="checkbox" + autocheck="false" prefKey="error"/> + <menuitem label="&btnConsoleWarnings;" type="checkbox" + autocheck="false" prefKey="warn"/> + <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false" + prefKey="info"/> + <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" + prefKey="log"/> + </menupopup> + </toolbarbutton> + </hbox> <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton" label="&btnClear.label;" tooltiptext="&btnClear.tooltip;" accesskey="&btnClear.accesskey;" @@ -160,17 +175,21 @@ placeholder="&filterOutput.placeholder;" tabindex="2"/> </toolbar> - <richlistbox class="hud-output-node" orient="vertical" flex="1" - seltype="multiple" context="output-contextmenu" - style="direction:ltr;" tabindex="1"/> - - <hbox class="jsterm-input-container" style="direction:ltr"> - <stack class="jsterm-stack-node" flex="1"> - <textbox class="jsterm-complete-node" multiline="true" rows="1" - tabindex="-1"/> - <textbox class="jsterm-input-node" multiline="true" rows="1" tabindex="0"/> - </stack> + <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip"> + <div xmlns="http://www.w3.org/1999/xhtml" id="output-container" + tabindex="0" role="document" aria-live="polite" /> </hbox> + <notificationbox id="webconsole-notificationbox"> + <hbox class="jsterm-input-container" style="direction:ltr"> + <stack class="jsterm-stack-node" flex="1"> + <textbox class="jsterm-complete-node devtools-monospace" + multiline="true" rows="1" tabindex="-1"/> + <textbox class="jsterm-input-node devtools-monospace" + multiline="true" rows="1" tabindex="0" + aria-autocomplete="list"/> + </stack> + </hbox> + </notificationbox> </vbox> <splitter class="devtools-side-splitter"/> |