diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/webconsole | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/webconsole')
519 files changed, 44898 insertions, 0 deletions
diff --git a/devtools/client/webconsole/.babelrc b/devtools/client/webconsole/.babelrc new file mode 100644 index 0000000000..af0f0c3d35 --- /dev/null +++ b/devtools/client/webconsole/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +}
\ No newline at end of file diff --git a/devtools/client/webconsole/console-commands.js b/devtools/client/webconsole/console-commands.js new file mode 100644 index 0000000000..0bc9e8edb5 --- /dev/null +++ b/devtools/client/webconsole/console-commands.js @@ -0,0 +1,103 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const l10n = require("gcli/l10n"); +loader.lazyRequireGetter(this, "gDevTools", + "devtools/client/framework/devtools", true); + +exports.items = [ + { + item: "command", + runAt: "client", + name: "splitconsole", + hidden: true, + buttonId: "command-button-splitconsole", + buttonClass: "command-button command-button-invertable", + tooltipText: l10n.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((newToolbox) => { + newToolbox.toggleSplitConsole(); + }); + } + return toolbox.toggleSplitConsole(); + } + }, + { + name: "console", + description: l10n.lookup("consoleDesc"), + manual: l10n.lookup("consoleManual") + }, + { + item: "command", + runAt: "client", + name: "console clear", + description: l10n.lookup("consoleclearDesc"), + exec: function (args, context) { + let toolbox = gDevTools.getToolbox(context.environment.target); + if (toolbox == null) { + return null; + } + + let panel = toolbox.getPanel("webconsole"); + if (panel == null) { + return null; + } + + let onceMessagesCleared = panel.hud.jsterm.once("messages-cleared"); + panel.hud.jsterm.clearOutput(); + return onceMessagesCleared; + } + }, + { + item: "command", + runAt: "client", + name: "console close", + description: l10n.lookup("consolecloseDesc"), + exec: function (args, context) { + // Don't return a value to GCLI + return gDevTools.closeToolbox(context.environment.target).then(() => {}); + } + }, + { + item: "command", + runAt: "client", + name: "console open", + description: l10n.lookup("consoleopenDesc"), + exec: function (args, context) { + const target = context.environment.target; + // Don't return a value to GCLI + return gDevTools.showToolbox(target, "webconsole").then(() => {}); + } + } +]; diff --git a/devtools/client/webconsole/console-output.js b/devtools/client/webconsole/console-output.js new file mode 100644 index 0000000000..52d8484943 --- /dev/null +++ b/devtools/client/webconsole/console-output.js @@ -0,0 +1,3638 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const {Ci, Cu} = require("chrome"); + +loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); +loader.lazyImporter(this, "escapeHTML", "resource://devtools/client/shared/widgets/VariablesView.jsm"); + +loader.lazyRequireGetter(this, "promise"); +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); +loader.lazyRequireGetter(this, "TableWidget", "devtools/client/shared/widgets/TableWidget", true); +loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); + +const { extend } = require("sdk/core/heritage"); +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; + +const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +const { getSourceNames } = require("devtools/client/shared/source-utils"); +const {Task} = require("devtools/shared/task"); +const l10n = new WebConsoleUtils.L10n(STRINGS_URI); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const {PluralForm} = require("devtools/shared/plural-form"); + +const MAX_STRING_GRIP_LENGTH = 36; +const {ELLIPSIS} = require("devtools/shared/l10n"); + +const validProtocols = /^(http|https|ftp|data|javascript|resource|chrome):/i; + +// 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, + SERVER: 7, + }, + + // 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". + /* eslint-disable no-multi-spaces */ + /* eslint-disable max-len */ + /* eslint-disable no-inline-comments */ + 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 + [ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging + ], + /* eslint-enable no-inline-comments */ + /* eslint-enable max-len */ + /* eslint-enable no-multi-spaces */ + + // The fragment of a CSS class name that identifies each category. + CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console", + "input", "output", "security", "server" ], + + // 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", + clear: "log", + trace: "log", + table: "log", + debug: "log", + dir: "log", + dirxml: "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); + }, + + openLocationInDebugger: function ({url, line}) { + return this.owner.owner.viewSourceInDebugger(url, line); + }, + + /** + * 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 + */ +var 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(); + } +}; + +/** + * 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 = 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; + }, +}); + +/** + * 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. + * - stack: array that tells the message source stack. + * - 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.stack = options.stack; + this.timestamp = options.timestamp || Date.now(); + this.prefix = options.prefix; + this.private = !!options.private; + + this._message = message; + this._className = options.className; + this._link = options.link; + this._linkCallback = options.linkCallback; + this._filterDuplicates = options.filterDuplicates; + + this._onClickCollapsible = this._onClickCollapsible.bind(this); +}; + +Messages.Simple.prototype = 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, + + /** + * Holds the stackframes received from the server. + * + * @private + * @type array + */ + stack: null, + + /** + * Message prefix + * @type string|null + */ + prefix: 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, + + /** + * The message's "attachment" element to be displayed under the message. + * Used for things like stack traces or tables in console.table(). + * + * @private + * @type DOMElement|null + */ + _attachment: 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; + }, + + /** + * 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"); + }, + + _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.prefix = this.prefix; + 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); + if (this.stack) { + icon.addEventListener("click", this._onClickCollapsible); + } + + let prefixNode; + if (this.prefix) { + prefixNode = this.document.createElementNS(XHTML_NS, "span"); + prefixNode.className = "prefix devtools-monospace"; + prefixNode.textContent = this.prefix + ":"; + } + + // 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(); + + 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); + if (prefixNode) { + this.element.appendChild(prefixNode); + } + + if (this.stack) { + let twisty = this.document.createElementNS(XHTML_NS, "a"); + twisty.className = "theme-twisty"; + twisty.href = "#"; + twisty.title = l10n.getStr("messageToggleDetails"); + twisty.addEventListener("click", this._onClickCollapsible); + this.element.appendChild(twisty); + this.collapsible = true; + this.element.setAttribute("collapsible", true); + } + + this.element.appendChild(body); + + this.element.clipboardText = this.element.textContent; + + if (this.private) { + this.element.setAttribute("private", true); + } + + // 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 bodyWrapper = this.document.createElementNS(XHTML_NS, "span"); + bodyWrapper.className = "message-body-wrapper"; + + let bodyFlex = this.document.createElementNS(XHTML_NS, "span"); + bodyFlex.className = "message-flex-body"; + bodyWrapper.appendChild(bodyFlex); + + let body = this.document.createElementNS(XHTML_NS, "span"); + body.className = "message-body devtools-monospace"; + bodyFlex.appendChild(body); + + let anchor, container = body; + 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); + body.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; + } + + // do this before repeatNode is rendered - it has no effect afterwards + this._repeatID.textContent += "|" + container.textContent; + + let repeatNode = this._renderRepeatNode(); + let location = this._renderLocation(); + + if (repeatNode) { + bodyFlex.appendChild(this.document.createTextNode(" ")); + bodyFlex.appendChild(repeatNode); + } + if (location) { + bodyFlex.appendChild(this.document.createTextNode(" ")); + bodyFlex.appendChild(location); + } + + bodyFlex.appendChild(this.document.createTextNode("\n")); + + if (this.stack) { + this._attachment = new Widgets.Stacktrace(this, this.stack).render().element; + } + + if (this._attachment) { + bodyWrapper.appendChild(this._attachment); + } + + return bodyWrapper; + }, + + /** + * 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, line, column }); + }, + + /** + * 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(); + }, + + /** + * 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); + } + }, +}); // 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 = JSON.stringify(messagePieces); + this._repeatID.actors = new Set(); // using a set to avoid duplicates +}; + +Messages.Extended.prototype = 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, options = {}) + { + if (piece instanceof Ci.nsIDOMNode) { + return piece; + } + if (typeof piece == "function") { + return piece(this); + } + + return this._renderValueGrip(piece, options); + }, + + /** + * 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. + * - shorten - boolean that tells the renderer to display a truncated + * grip. + * @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 unshortenedGrip = grip; + if (options.shorten) { + grip = this.shortenValueGrip(grip); + } + + 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, unshortenedGrip).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. + * @param string [errorDocLink] + * Optional error doc URL to link to. + */ +Messages.JavaScriptEvalOutput = function (evalResponse, errorMessage, errorDocLink) +{ + 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 (typeof (errorMessage) !== "undefined") { + 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, + }; + + let messages = [msg]; + if (errorDocLink) { + messages.push(errorDocLink); + } + + Messages.Extended.call(this, messages, 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: packet.category || "webdev", + severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], + prefix: packet.prefix, + 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.stack = this._repeatID.stacktrace = packet.stacktrace; + this._styles = packet.styles || []; +}; + +Messages.ConsoleGeneric.prototype = extend(Messages.Extended.prototype, { + _styles: null, + + _renderBodyPieceSeparator: function () + { + return this.document.createTextNode(" "); + }, + + render: function () + { + let result = this.document.createDocumentFragment(); + this._renderBodyPieces(result); + + this._message = result; + this._stacktrace = null; + + Messages.Simple.prototype.render.call(this); + + return this; + }, + + _renderBodyPieces: function (container) + { + let lastStyle = null; + let stylePieces = this._styles.length > 0 ? this._styles.length : 1; + + for (let i = 0; i < this._messagePieces.length; i++) { + // Pieces with an associated style definition come from "%c" formatting. + // For body pieces beyond that, add a separator before each one. + if (i >= stylePieces) { + container.appendChild(this._renderBodyPieceSeparator()); + } + + let piece = this._messagePieces[i]; + let style = this._styles[i]; + + // No long string support. + lastStyle = (style && typeof style == "string") ? + this.cleanupStyle(style) : null; + + container.appendChild(this._renderBodyPiece(piece, lastStyle)); + } + + this._messagePieces = null; + this._styles = null; + }, + + _renderBodyPiece: function (piece, style) + { + // Skip quotes for top-level strings. + let options = { noStringQuotes: true }; + let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece, options); + let result = elem; + + if (style) { + if (elem.nodeType == nodeConstants.ELEMENT_NODE) { + elem.style = style; + } else { + let span = this.document.createElementNS(XHTML_NS, "span"); + span.style = style; + span.appendChild(elem); + result = span; + } + } + + return result; + }, + + /** + * 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: packet.category || "webdev", + severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], + private: packet.private, + filterDuplicates: true, + location: { + url: packet.filename, + line: packet.lineNumber, + }, + }; + + Messages.Simple.call(this, null, options); + + this._repeatID.consoleApiLevel = packet.level; + this._stacktrace = this._repeatID.stacktrace = packet.stacktrace; + this._arguments = packet.arguments; +}; + +Messages.ConsoleTrace.prototype = 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 () { + this._message = this._renderMessage(); + this._attachment = this._renderStack(); + + Messages.Simple.prototype.render.apply(this, arguments); + this.element.setAttribute("open", true); + return this; + }, + + /** + * Render the console messageNode + */ + _renderMessage: 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 frag = this.document.createDocumentFragment(); + frag.appendChild(cmvar); + frag.appendChild(this.document.createTextNode(".")); + frag.appendChild(cmprop); + frag.appendChild(this.document.createTextNode("():")); + + return frag; + }, + + /** + * Render the stack frames. + * + * @private + * @return DOMElement + */ + _renderStack: function () { + return new Widgets.Stacktrace(this, this._stacktrace).render().element; + }, +}); // 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: packet.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._renderMessage = this._renderMessage.bind(this); + Messages.Extended.call(this, [this._renderMessage], options); + + this._repeatID.consoleApiLevel = packet.level; + this._arguments = packet.arguments; +}; + +Messages.ConsoleTable.prototype = 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" && + data.class != "WeakMap" && data.class != "WeakSet") { + 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" || data.class == "WeakMap") { + 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" || data.class == "WeakSet") { + 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 () + { + this._attachment = this._renderTable(); + Messages.Extended.prototype.render.apply(this, arguments); + this.element.setAttribute("open", true); + return this; + }, + + _renderMessage: 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 frag = this.document.createDocumentFragment(); + frag.appendChild(cmvar); + frag.appendChild(this.document.createTextNode(".")); + frag.appendChild(cmprop); + frag.appendChild(this.document.createTextNode("():")); + + return frag; + }, + + /** + * Render the table. + * + * @private + * @return DOMElement + */ + _renderTable: function () { + let result = this.document.createElementNS(XHTML_NS, "div"); + + 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; + }, +}); // Messages.ConsoleTable.prototype + +var 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 = 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. + * @param string unshortenedStr + * The unshortened form of the string, if it was shortened. + */ +Widgets.URLString = function (message, str, unshortenedStr) +{ + Widgets.BaseWidget.call(this, message); + this.str = str; + this.unshortenedStr = unshortenedStr; +}; + +Widgets.URLString.prototype = 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 separated the tokens. + let tokens = this.str.split(/\s+/); + let textStart = 0; + let tokenStart; + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + let unshortenedToken; + tokenStart = this.str.indexOf(token, textStart); + if (this._isURL(token)) { + // The last URL in the string might be shortened. If so, get the + // real URL so the rendered link can point to it. + if (i === tokens.length - 1 && this.unshortenedStr) { + unshortenedToken = this.unshortenedStr.slice(tokenStart).split(/\s+/, 1)[0]; + } + this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart))); + textStart = tokenStart + token.length; + this.element.appendChild(this._renderURL(token, unshortenedToken)); + } + } + + // 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 { + if (!validProtocols.test(token)) { + return false; + } + new URL(token); + return true; + } catch (e) { + return false; + } + }, + + /** + * Renders a string as a URL. + * + * @param string url + * The string to be rendered as a url. + * @param string fullUrl + * The unshortened form of the URL, if it was shortened. + * @return DOMElement + * An element containing the rendered string. + */ + _renderURL: function (url, fullUrl) + { + let unshortened = fullUrl || url; + let result = this.el("a", { + class: "url", + title: unshortened, + href: unshortened, + 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 = 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 valueElem = this.message._renderValueGrip(value, { concise: true, shorten: 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; + } + + options.onContextMenu = options.onContextMenu || this._onContextMenu; + + let anchor = this.el("a", { + class: options.className, + draggable: false, + href: options.href || "#", + }, text); + + this.message._addLinkCallback(anchor, options.onClick); + + anchor.addEventListener("contextmenu", options.onContextMenu.bind(this)); + + if (options.appendTo) { + options.appendTo.appendChild(anchor); + } else if (!("appendTo" in options) && this.element) { + this.element.appendChild(anchor); + } + + return anchor; + }, + + openObjectInVariablesView: function () + { + this.output.openVariablesView({ + label: VariablesView.getString(this.objectActor, { concise: true }), + objectActor: this.objectActor, + autofocus: true, + }); + }, + + storeObjectInWindow: function () + { + let evalString = `{ let i = 0; + while (this.hasOwnProperty("temp" + i) && i < 1000) { + i++; + } + this["temp" + i] = _self; + "temp" + i; + }`; + let options = { + selectedObjectActor: this.objectActor.actor, + }; + + this.output.owner.jsterm.requestEvaluation(evalString, options).then((res) => { + this.output.owner.jsterm.focus(); + this.output.owner.jsterm.setInputValue(res.result); + }); + }, + + /** + * The click event handler for objects shown inline. + * @private + */ + _onClick: function () + { + this.openObjectInVariablesView(); + }, + + _onContextMenu: function (ev) { + // TODO offer a nice API for the context menu. + // Probably worth to take a look at Firebug's way + // https://github.com/firebug/firebug/blob/master/extension/content/firebug/chrome/menu.js + let doc = ev.target.ownerDocument; + let cmPopup = doc.getElementById("output-contextmenu"); + + let openInVarViewCmd = doc.getElementById("menu_openInVarView"); + let openVarView = this.openObjectInVariablesView.bind(this); + openInVarViewCmd.addEventListener("command", openVarView); + openInVarViewCmd.removeAttribute("disabled"); + cmPopup.addEventListener("popuphiding", function onPopupHiding() { + cmPopup.removeEventListener("popuphiding", onPopupHiding); + openInVarViewCmd.removeEventListener("command", openVarView); + openInVarViewCmd.setAttribute("disabled", "true"); + }); + + // 'Store as global variable' command isn't supported on pre-44 servers, + // so remove it from the menu in that case. + let storeInGlobalCmd = doc.getElementById("menu_storeAsGlobal"); + if (!this.output.webConsoleClient.traits.selectedObjectActor) { + storeInGlobalCmd.remove(); + } else if (storeInGlobalCmd) { + let storeObjectInWindow = this.storeObjectInWindow.bind(this); + storeInGlobalCmd.addEventListener("command", storeObjectInWindow); + storeInGlobalCmd.removeAttribute("disabled"); + cmPopup.addEventListener("popuphiding", function onPopupHiding() { + cmPopup.removeEventListener("popuphiding", onPopupHiding); + storeInGlobalCmd.removeEventListener("command", storeObjectInWindow); + storeInGlobalCmd.setAttribute("disabled", "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 = 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(")"); + }, + + _onClick: function () { + let location = this.objectActor.location; + if (location && IGNORED_SOURCE_URLS.indexOf(location.url) === -1) { + this.output.openLocationInDebugger(location); + } + else { + this.openObjectInVariablesView(); + } + } +}); // 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 elem = this.message._renderValueGrip(item, { concise: true, shorten: 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 = getSourceNames(url)[this.options.concise ? "short" : "long"]; + 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.theme-fg-color6", + 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 nodeConstants.DOCUMENT_NODE: + case nodeConstants.ATTRIBUTE_NODE: + case nodeConstants.TEXT_NODE: + case nodeConstants.COMMENT_NODE: + case nodeConstants.DOCUMENT_FRAGMENT_NODE: + case nodeConstants.ELEMENT_NODE: + return true; + default: + return false; + } + }, + + render: function () + { + switch (this.objectActor.preview.nodeType) { + case nodeConstants.DOCUMENT_NODE: + this._renderDocumentNode(); + break; + case nodeConstants.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 nodeConstants.TEXT_NODE: + this._renderTextNode(); + break; + case nodeConstants.COMMENT_NODE: + this._renderCommentNode(); + break; + case nodeConstants.DOCUMENT_FRAGMENT_NODE: + this._renderDocumentFragmentNode(); + break; + case nodeConstants.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.theme-fg-color6", escapeHTML(value))); + this._text("\"", fragment); + + 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"); + + this._text("<"); + let openTag = this.el("span.cm-tag"); + 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); + } + } + + this._text(">"); + + // 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, e => console.error(e)); + }, + + /** + * 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 nodeConstants.ELEMENT_NODE). + */ + linkToInspector: Task.async(function* () + { + if (this._linkedToInspector) { + return; + } + + // Checking the node type + if (this.objectActor.preview.nodeType !== nodeConstants.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) { + // In cases like the browser console, there is no toolbox. + return; + } + + // 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"); + } + + // Check it again as this method is async! + if (this._linkedToInspector) { + return; + } + this._linkedToInspector = true; + + 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"); + }), + + /** + * 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, e => console.error(e)); + }, + + /** + * 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 = promise.defer(); + this.toolbox.inspector.once("inspector-updated", onReady.resolve); + yield this.toolbox.selection.setNodeFront(this._nodeFront, "console"); + yield onReady.promise; + } 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); + + if (this._linkedToInspector) { + this.unhighlightDomNode().then(() => { + this.toolbox = null; + this._nodeFront = null; + }); + } else { + 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 + +/* + * A renderer used for wrapped primitive objects. + */ + +function WrappedPrimitiveRenderer() { + let { ownProperties, safeGetterValues } = this.objectActor.preview || {}; + if ((!ownProperties && !safeGetterValues) || this.options.concise) { + this._renderConciseObject(); + return; + } + + this._renderObjectPrefix(); + + let elem = + this.message._renderValueGrip(this.objectActor.preview.wrappedValue); + this.element.appendChild(elem); + + this._renderObjectProperties(this.element, true); + this._renderObjectSuffix(); +} + +/** + * The widget used for displaying Boolean previews. + */ +Widgets.ObjectRenderers.add({ + byClass: "Boolean", + + render: WrappedPrimitiveRenderer, +}); + +/** + * The widget used for displaying Number previews. + */ +Widgets.ObjectRenderers.add({ + byClass: "Number", + + render: WrappedPrimitiveRenderer, +}); + +/** + * The widget used for displaying String previews. + */ +Widgets.ObjectRenderers.add({ + byClass: "String", + + render: WrappedPrimitiveRenderer, +}); + +/** + * 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. + * @param object options + * Options, such as noStringQuotes + */ +Widgets.LongString = function (message, longStringActor, options) +{ + Widgets.BaseWidget.call(this, message); + this.longStringActor = longStringActor; + this.noStringQuotes = (options && "noStringQuotes" in options) ? + options.noStringQuotes : !this.message._quoteStrings; + + this._onClick = this._onClick.bind(this); + this._onSubstring = this._onSubstring.bind(this); +}; + +Widgets.LongString.prototype = 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.noStringQuotes, + 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) { + console.error("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 = extend(Widgets.BaseWidget.prototype, { + /** + * The stackframes received from the server. + * @type array + */ + stacktrace: null, + + render() { + if (this.element) { + return this; + } + + let result = this.element = this.document.createElementNS(XHTML_NS, "div"); + result.className = "stacktrace devtools-monospace"; + + if (this.stacktrace) { + this.output.owner.ReactDOM.render(this.output.owner.StackTraceView({ + stacktrace: this.stacktrace, + onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame) + }), result); + } + + return this; + } +}); + +/** + * 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 = 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, { + wrapTextInElements: true, + 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/devtools/client/webconsole/hudservice.js b/devtools/client/webconsole/hudservice.js new file mode 100644 index 0000000000..46b4f2a139 --- /dev/null +++ b/devtools/client/webconsole/hudservice.js @@ -0,0 +1,718 @@ +/* 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"); + +var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +var { extend } = require("sdk/core/heritage"); +var {TargetFactory} = require("devtools/client/framework/target"); +var {Tools} = require("devtools/client/definitions"); +const { Task } = require("devtools/shared/task"); +var promise = require("promise"); +var Services = require("Services"); + +loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry"); +loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole", true); +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); +loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); +loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "showDoorhanger", "devtools/client/shared/doorhanger", true); +loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source"); + +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; +var l10n = new WebConsoleUtils.L10n(STRINGS_URI); + +const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + +// The preference prefix for all of the Browser Console filters. +const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter."; + +var gHudId = 0; + +// The HUD service + +function HUD_SERVICE() +{ + this.consoles = new Map(); + this.lastFinishedRequest = { callback: null }; +} + +HUD_SERVICE.prototype = +{ + _browserConsoleID: null, + _browserConsoleDefer: null, + + /** + * Keeps a reference for each Web Console / Browser Console that is created. + * @type Map + */ + consoles: 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. + * + * @type object + * Includes a property named |callback|. Assign the function to the + * |callback| property of this object. + */ + lastFinishedRequest: null, + + /** + * Get the current context, which is the main application window. + * + * @returns nsIDOMWindow + */ + currentContext: function HS_currentContext() { + return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + }, + + /** + * Open a Web Console for the given target. + * + * @see devtools/framework/target.js for details about targets. + * + * @param object aTarget + * The target that the web console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the web console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the web console owner. + * @return object + * A promise object for the opening of the new WebConsole instance. + */ + openWebConsole: + function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow) + { + let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow); + this.consoles.set(hud.hudId, hud); + return hud.init(); + }, + + /** + * Open a Browser Console for the given target. + * + * @see devtools/framework/target.js for details about targets. + * + * @param object aTarget + * The target that the browser console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the browser console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the browser console owner. + * @return object + * A promise object for the opening of the new BrowserConsole instance. + */ + openBrowserConsole: + function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow) + { + let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow); + this._browserConsoleID = hud.hudId; + this.consoles.set(hud.hudId, hud); + return hud.init(); + }, + + /** + * Returns the Web Console object associated to a content window. + * + * @param nsIDOMWindow aContentWindow + * @returns object + */ + getHudByWindow: function HS_getHudByWindow(aContentWindow) + { + for (let [hudId, hud] of this.consoles) { + let target = hud.target; + if (target && target.tab && target.window === aContentWindow) { + return hud; + } + } + return null; + }, + + /** + * Returns the console instance for a given id. + * + * @param string aId + * @returns Object + */ + getHudReferenceById: function HS_getHudReferenceById(aId) + { + return this.consoles.get(aId); + }, + + /** + * 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. + */ + getOpenWebConsole: function HS_getOpenWebConsole() + { + let tab = this.currentContext().gBrowser.selectedTab; + if (!tab || !TargetFactory.isKnownTab(tab)) { + return null; + } + let target = TargetFactory.forTab(tab); + let toolbox = gDevTools.getToolbox(target); + let panel = toolbox ? toolbox.getPanel("webconsole") : null; + return panel ? panel.hud : null; + }, + + /** + * Toggle the Browser Console. + */ + toggleBrowserConsole: function 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(); + } + DebuggerServer.allowChromeProcess = true; + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + return client.connect() + .then(() => client.getProcess()) + .then(aResponse => { + // Set chrome:false in order to attach to the target + // (i.e. send an `attach` request to the chrome actor) + return { form: aResponse.form, client: client, chrome: false }; + }); + } + + let target; + function getTarget(aConnection) + { + return TargetFactory.forRemoteTab(aConnection); + } + + function openWindow(aTarget) + { + target = aTarget; + + let deferred = promise.defer(); + + let win = Services.ww.openWindow(null, 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) => { + return this.openBrowserConsole(target, aWindow, aWindow) + .then((aBrowserConsole) => { + this._browserConsoleDefer.resolve(aBrowserConsole); + this._browserConsoleDefer = null; + }); + }, console.error.bind(console)); + + 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. + * + * @return object|null + * A BrowserConsole instance or null if the Browser Console is not + * open. + */ + getBrowserConsole: function HS_getBrowserConsole() + { + return this.getHudReferenceById(this._browserConsoleID); + }, +}; + + +/** + * A WebConsole instance is an interactive console initialized *per target* + * that displays console log data as well as provides an interactive terminal to + * manipulate the target's document content. + * + * This object only wraps the iframe that holds the Web Console UI. This is + * meant to be an integration point between the Firefox UI and the Web Console + * UI and features. + * + * @constructor + * @param object aTarget + * The target that the web console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the web console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the web console owner. + */ +function WebConsole(aTarget, aIframeWindow, aChromeWindow) +{ + this.iframeWindow = aIframeWindow; + this.chromeWindow = aChromeWindow; + this.hudId = "hud_" + ++gHudId; + this.target = aTarget; + + this.browserWindow = this.chromeWindow.top; + + let element = this.browserWindow.document.documentElement; + if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) { + this.browserWindow = HUDService.currentContext(); + } + + this.ui = new WebConsoleFrame(this); +} + +WebConsole.prototype = { + iframeWindow: null, + chromeWindow: null, + browserWindow: null, + hudId: null, + target: null, + ui: null, + _browserConsole: false, + _destroyer: null, + + /** + * 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. + * + * @type function + */ + get lastFinishedRequestCallback() + { + return 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. + * @type nsIDOMElement + */ + get mainPopupSet() + { + return this.chromeUtilsWindow.document.getElementById("mainPopupSet"); + }, + + /** + * Getter for the output element that holds messages we display. + * @type nsIDOMElement + */ + get outputNode() + { + return this.ui ? this.ui.outputNode : null; + }, + + get gViewSourceUtils() + { + return this.chromeUtilsWindow.gViewSourceUtils; + }, + + /** + * Initialize the Web Console instance. + * + * @return object + * A promise for the initialization. + */ + init: function WC_init() + { + return this.ui.init().then(() => this); + }, + + /** + * Retrieve the Web Console panel title. + * + * @return string + * The Web Console panel title. + */ + getPanelTitle: function WC_getPanelTitle() + { + let url = this.ui ? this.ui.contentLocation : ""; + return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]); + }, + + /** + * The JSTerm object that manages the console's input. + * @see webconsole.js::JSTerm + * @type object + */ + get jsterm() + { + return this.ui ? this.ui.jsterm : null; + }, + + /** + * The clear output button handler. + * @private + */ + _onClearButton: function WC__onClearButton() + { + if (this.target.isLocalTab) { + this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab); + } + }, + + /** + * Alias for the WebConsoleFrame.setFilterState() method. + * @see webconsole.js::WebConsoleFrame.setFilterState() + */ + setFilterState: function WC_setFilterState() + { + this.ui && this.ui.setFilterState.apply(this.ui, arguments); + }, + + /** + * Open a link in a new tab. + * + * @param string aLink + * The URL you want to open in a new tab. + */ + openLink: function WC_openLink(aLink) + { + this.chromeUtilsWindow.openUILinkIn(aLink, "tab"); + }, + + /** + * Open a link in Firefox's view source. + * + * @param string aSourceURL + * The URL of the file. + * @param integer aSourceLine + * The line number which should be highlighted. + */ + viewSource: function WC_viewSource(aSourceURL, aSourceLine) { + // Attempt to access view source via a browser first, which may display it in + // a tab, if enabled. + let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + if (browserWin && browserWin.BrowserViewSourceOfDocument) { + return browserWin.BrowserViewSourceOfDocument({ + URL: aSourceURL, + lineNumber: aSourceLine + }); + } + this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0); + }, + + /** + * Tries to open a Stylesheet file related to the web page for the web console + * instance in the Style Editor. If the file is not found, it is opened in + * source view instead. + * + * Manually handle the case where toolbox does not exist (Browser Console). + * + * @param string aSourceURL + * The URL of the file. + * @param integer aSourceLine + * The line number which you want to place the caret. + */ + viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) { + let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + this.viewSource(aSourceURL, aSourceLine); + return; + } + toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine); + }, + + /** + * Tries to open a JavaScript file related to the web page for the web console + * instance in the Script Debugger. If the file is not found, it is opened in + * source view instead. + * + * Manually handle the case where toolbox does not exist (Browser Console). + * + * @param string aSourceURL + * The URL of the file. + * @param integer aSourceLine + * The line number which you want to place the caret. + */ + viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) { + let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + this.viewSource(aSourceURL, aSourceLine); + return; + } + toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => { + this.ui.emit("source-in-debugger-opened"); + }); + }, + + /** + * 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, aSourceLine) { + viewSource.viewSourceInScratchpad(aSourceURL, aSourceLine); + }, + + /** + * Retrieve information about the JavaScript debugger's stackframes list. This + * is used to allow the Web Console to evaluate code in the selected + * stackframe. + * + * @return object|null + * An object which holds: + * - frames: the active ThreadClient.cachedFrames array. + * - selected: depth/index of the selected stackframe in the debugger + * UI. + * If the debugger is not open or if it's not paused, then |null| is + * returned. + */ + getDebuggerFrames: function WC_getDebuggerFrames() + { + let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + return null; + } + let panel = toolbox.getPanel("jsdebugger"); + + if (!panel) { + return null; + } + + return panel.getFrames(); + }, + + /** + * 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. + * + * @return object + * A promise object that is resolved once the Web Console is closed. + */ + destroy: function WC_destroy() + { + if (this._destroyer) { + return this._destroyer.promise; + } + + HUDService.consoles.delete(this.hudId); + + this._destroyer = promise.defer(); + + // The document may already be removed + if (this.chromeUtilsWindow && this.mainPopupSet) { + let popupset = this.mainPopupSet; + let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]"); + for (let panel of panels) { + panel.hidePopup(); + } + } + + let onDestroy = Task.async(function* () { + if (!this._browserConsole) { + try { + yield this.target.activeTab.focus(); + } + catch (ex) { + // Tab focus can fail if the tab or target is closed. + } + } + + let id = WebConsoleUtils.supportsString(this.hudId); + Services.obs.notifyObservers(id, "web-console-destroyed", null); + this._destroyer.resolve(null); + }.bind(this)); + + if (this.ui) { + this.ui.destroy().then(onDestroy); + } + else { + onDestroy(); + } + + return this._destroyer.promise; + }, +}; + +/** + * A BrowserConsole instance is an interactive console initialized *per target* + * that displays console log data as well as provides an interactive terminal to + * manipulate the target's document content. + * + * This object only wraps the iframe that holds the Browser Console UI. This is + * meant to be an integration point between the Firefox UI and the Browser Console + * UI and features. + * + * @constructor + * @param object aTarget + * The target that the browser console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the browser console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the browser console owner. + */ +function BrowserConsole() +{ + WebConsole.apply(this, arguments); + this._telemetry = new Telemetry(); +} + +BrowserConsole.prototype = extend(WebConsole.prototype, { + _browserConsole: true, + _bc_init: null, + _bc_destroyer: null, + + $init: WebConsole.prototype.init, + + /** + * Initialize the Browser Console instance. + * + * @return object + * A promise for the initialization. + */ + init: function BC_init() + { + if (this._bc_init) { + return this._bc_init; + } + + this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX; + + let window = this.iframeWindow; + + // Make sure that the closing of the Browser Console window destroys this + // instance. + let onClose = () => { + window.removeEventListener("unload", onClose); + window.removeEventListener("focus", onFocus); + this.destroy(); + }; + window.addEventListener("unload", onClose); + + 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; + }, + + $destroy: WebConsole.prototype.destroy, + + /** + * Destroy the object. + * + * @return object + * A promise object that is resolved once the Browser Console is closed. + */ + destroy: function BC_destroy() + { + if (this._bc_destroyer) { + return this._bc_destroyer.promise; + } + + this._telemetry.toolClosed("browserconsole"); + + this._bc_destroyer = promise.defer(); + + let chromeWindow = this.chromeWindow; + this.$destroy().then(() => + this.target.client.close().then(() => { + HUDService._browserConsoleID = null; + chromeWindow.close(); + this._bc_destroyer.resolve(null); + })); + + return this._bc_destroyer.promise; + }, +}); + +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/devtools/client/webconsole/jsterm.js b/devtools/client/webconsole/jsterm.js new file mode 100644 index 0000000000..8e3259afa3 --- /dev/null +++ b/devtools/client/webconsole/jsterm.js @@ -0,0 +1,1766 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const {Utils: WebConsoleUtils} = + require("devtools/client/webconsole/utils"); +const promise = require("promise"); +const Debugger = require("Debugger"); +const Services = require("Services"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +loader.lazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); +loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true); +loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true); +loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true); +loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage"); +loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); +loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); +loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); + +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; +var l10n = new WebConsoleUtils.L10n(STRINGS_URI); + +// Constants used for defining the direction of JSTerm input history navigation. +const HISTORY_BACK = -1; +const HISTORY_FORWARD = 1; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + +const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; + +const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; +const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; + +/** + * Create a JSTerminal (a JavaScript command line). This is attached to an + * existing HeadsUpDisplay (a Web Console instance). This code is responsible + * with handling command line input, code evaluation and result output. + * + * @constructor + * @param object webConsoleFrame + * The WebConsoleFrame object that owns this JSTerm instance. + */ +function JSTerm(webConsoleFrame) { + this.hud = webConsoleFrame; + this.hudId = this.hud.hudId; + this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT); + + this.lastCompletion = { value: null }; + this._loadHistory(); + + this._objectActorsInVariablesViews = new Map(); + + this._keyPress = this._keyPress.bind(this); + 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); +} + +JSTerm.prototype = { + SELECTED_FRAME: -1, + + /** + * Load the console history from previous sessions. + * @private + */ + _loadHistory: function () { + this.history = []; + this.historyIndex = this.historyPlaceHolder = 0; + + this.historyLoaded = asyncStorage.getItem("webConsoleHistory") + .then(value => { + if (Array.isArray(value)) { + // Since it was gotten asynchronously, there could be items already in + // the history. It's not likely but stick them onto the end anyway. + this.history = value.concat(this.history); + + // Holds the number of entries in history. This value is incremented + // in this.execute(). + this.historyIndex = this.history.length; + + // Holds the index of the history entry that the user is currently + // viewing. This is reset to this.history.length when this.execute() + // is invoked. + this.historyPlaceHolder = this.history.length; + } + }, console.error); + }, + + /** + * Clear the console history altogether. Note that this will not affect + * other consoles that are already opened (since they have their own copy), + * but it will reset the array for all newly-opened consoles. + * @returns Promise + * Resolves once the changes have been persisted. + */ + clearHistory: function () { + this.history = []; + this.historyIndex = this.historyPlaceHolder = 0; + return this.storeHistory(); + }, + + /** + * Stores the console history for future console instances. + * @returns Promise + * Resolves once the changes have been persisted. + */ + storeHistory: function () { + return asyncStorage.setItem("webConsoleHistory", this.history); + }, + + /** + * Stores the data for the last completion. + * @type object + */ + 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 + */ + sidebar: null, + + /** + * The Variables View instance shown in the sidebar. + * @private + * @type object + */ + _variablesView: null, + + /** + * Tells if you want the variables view UI updates to be lazy or not. Tests + * disable lazy updates. + * + * @private + * @type boolean + */ + _lazyVariablesView: true, + + /** + * Holds a map between VariablesView instances and sets of ObjectActor IDs + * that have been retrieved from the server. This allows us to release the + * objects when needed. + * + * @private + * @type Map + */ + _objectActorsInVariablesViews: null, + + /** + * Last input value. + * @type string + */ + lastInputValue: "", + + /** + * Tells if the input node changed since the last focus. + * + * @private + * @type boolean + */ + _inputChanged: false, + + /** + * Tells if the autocomplete popup was navigated since the last open. + * + * @private + * @type boolean + */ + _autocompletePopupNavigated: false, + + /** + * History of code that was executed. + * @type array + */ + history: null, + autocompletePopup: null, + inputNode: null, + completeNode: null, + + /** + * Getter for the element that holds the messages we display. + * @type nsIDOMElement + */ + get outputNode() { + return this.hud.outputNode; + }, + + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() { + return this.hud.webConsoleClient; + }, + + COMPLETE_FORWARD: 0, + COMPLETE_BACKWARD: 1, + COMPLETE_HINT_ONLY: 2, + COMPLETE_PAGEUP: 3, + COMPLETE_PAGEDOWN: 4, + + /** + * Initialize the JSTerminal UI. + */ + init: function () { + let autocompleteOptions = { + onSelect: this.onAutocompleteSelect.bind(this), + onClick: this.acceptProposedCompletion.bind(this), + listId: "webConsole_autocompletePopupListBox", + position: "top", + theme: "auto", + autoSelect: true + }; + + let doc = this.hud.document; + let toolbox = gDevTools.getToolbox(this.hud.owner.target); + let tooltipDoc = toolbox ? toolbox.doc : doc; + // The popup will be attached to the toolbox document or HUD document in the case + // such as the browser console which doesn't have a toolbox. + this.autocompletePopup = new AutocompletePopup(tooltipDoc, autocompleteOptions); + + let inputContainer = doc.querySelector(".jsterm-input-container"); + this.completeNode = doc.querySelector(".jsterm-complete-node"); + this.inputNode = doc.querySelector(".jsterm-input-node"); + + if (this.hud.isBrowserConsole && + !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); + }, + + focus: function () { + if (!this.inputNode.getAttribute("focused")) { + this.inputNode.focus(); + } + }, + + /** + * The JavaScript evaluation response handler. + * + * @private + * @param function [callback] + * Optional function to invoke when the evaluation result is added to + * the output. + * @param object response + * The message received from the server. + */ + _executeResultCallback: function (callback, response) { + if (!this.hud) { + return; + } + if (response.error) { + console.error("Evaluation error " + response.error + ": " + + response.message); + return; + } + let errorMessage = response.exceptionMessage; + let errorDocURL = response.exceptionDocURL; + + let errorDocLink; + if (errorDocURL) { + errorMessage += " "; + errorDocLink = this.hud.document.createElementNS(XHTML_NS, "a"); + errorDocLink.className = "learn-more-link webconsole-learn-more-link"; + errorDocLink.textContent = `[${l10n.getStr("webConsoleMoreInfoLabel")}]`; + errorDocLink.title = errorDocURL.split("?")[0]; + errorDocLink.href = "#"; + errorDocLink.draggable = false; + errorDocLink.addEventListener("click", () => { + this.hud.owner.openLink(errorDocURL); + }); + } + + // Wrap thrown strings in Error objects, so `throw "foo"` outputs + // "Error: foo" + if (typeof response.exception === "string") { + errorMessage = new Error(errorMessage).toString(); + } + let result = response.result; + let helperResult = response.helperResult; + let helperHasRawOutput = !!(helperResult || {}).rawOutput; + + if (helperResult && helperResult.type) { + switch (helperResult.type) { + case "clearOutput": + this.clearOutput(); + break; + case "clearHistory": + this.clearHistory(); + break; + case "inspectObject": + this.openVariablesView({ + label: + VariablesView.getString(helperResult.object, { concise: true }), + objectActor: helperResult.object, + }); + break; + case "error": + try { + errorMessage = l10n.getStr(helperResult.message); + } catch (ex) { + errorMessage = helperResult.message; + } + break; + case "help": + this.hud.owner.openLink(HELP_URL); + break; + case "copyValueToClipboard": + clipboardHelper.copyString(helperResult.value); + break; + } + } + + // Hide undefined results coming from JSTerm helper functions. + if (!errorMessage && result && typeof result == "object" && + result.type == "undefined" && + helperResult && !helperHasRawOutput) { + callback && callback(); + return; + } + + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + this.hud.newConsoleOutput.dispatchMessageAdd(response, true).then(callback); + return; + } + let msg = new Messages.JavaScriptEvalOutput(response, + errorMessage, errorDocLink); + this.hud.output.addMessage(msg); + + if (callback) { + let oldFlushCallback = this.hud._flushCallback; + this.hud._flushCallback = () => { + callback(msg.element); + if (oldFlushCallback) { + oldFlushCallback(); + this.hud._flushCallback = oldFlushCallback; + return true; + } + + return false; + }; + } + + msg._objectActors = new Set(); + + if (WebConsoleUtils.isActorGrip(response.exception)) { + msg._objectActors.add(response.exception.actor); + } + + if (WebConsoleUtils.isActorGrip(result)) { + msg._objectActors.add(result.actor); + } + }, + + /** + * Execute a string. Execution happens asynchronously in the content process. + * + * @param string [executeString] + * The string you want to execute. If this is not provided, the current + * user input is used - taken from |this.getInputValue()|. + * @param function [callback] + * 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 (executeString, callback) { + let deferred = promise.defer(); + let resultCallback; + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + resultCallback = (msg) => deferred.resolve(msg); + } else { + resultCallback = (msg) => { + deferred.resolve(msg); + if (callback) { + callback(msg); + } + }; + } + + // attempt to execute the content of the inputNode + executeString = executeString || this.getInputValue(); + if (!executeString) { + return null; + } + + let selectedNodeActor = null; + let inspectorSelection = this.hud.owner.getInspectorSelection(); + if (inspectorSelection && inspectorSelection.nodeFront) { + selectedNodeActor = inspectorSelection.nodeFront.actorID; + } + + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types"); + let message = new ConsoleCommand({ + messageText: executeString, + }); + this.hud.proxy.dispatchMessageAdd(message); + } else { + let message = new Messages.Simple(executeString, { + category: "input", + severity: "log", + }); + this.hud.output.addMessage(message); + } + let onResult = this._executeResultCallback.bind(this, resultCallback); + + let options = { + frame: this.SELECTED_FRAME, + selectedNodeActor: selectedNodeActor, + }; + + this.requestEvaluation(executeString, options).then(onResult, onResult); + + // Append a new value in the history of executed code, or overwrite the most + // recent entry. The most recent entry may contain the last edited input + // value that was not evaluated yet. + this.history[this.historyIndex++] = executeString; + this.historyPlaceHolder = this.history.length; + + if (this.history.length > this.inputHistoryCount) { + this.history.splice(0, this.history.length - this.inputHistoryCount); + this.historyIndex = this.historyPlaceHolder = this.history.length; + } + this.storeHistory(); + WebConsoleUtils.usageCount++; + this.setInputValue(""); + this.clearCompletion(); + return deferred.promise; + }, + + /** + * Request a JavaScript string evaluation from the server. + * + * @param string str + * String to execute. + * @param object [options] + * Options for evaluation: + * - bindObjectActor: tells the ObjectActor ID for which you want to do + * the evaluation. The Debugger.Object of the OA will be bound to + * |_self| during evaluation, such that it's usable in the string you + * execute. + * - frame: tells the stackframe depth to evaluate the string in. If + * the jsdebugger is paused, you can pick the stackframe to be used for + * evaluation. Use |this.SELECTED_FRAME| to always pick the + * user-selected stackframe. + * If you do not provide a |frame| the string will be evaluated in the + * global content window. + * - 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. + */ + requestEvaluation: function (str, options = {}) { + let deferred = promise.defer(); + + function onResult(response) { + if (!response.error) { + deferred.resolve(response); + } else { + deferred.reject(response); + } + } + + let frameActor = null; + if ("frame" in options) { + frameActor = this.getFrameActor(options.frame); + } + + let evalOptions = { + bindObjectActor: options.bindObjectActor, + frameActor: frameActor, + selectedNodeActor: options.selectedNodeActor, + selectedObjectActor: options.selectedObjectActor, + }; + + this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions); + return deferred.promise; + }, + + /** + * Retrieve the FrameActor ID given a frame depth. + * + * @param number frame + * Frame depth. + * @return string|null + * The FrameActor ID for the given frame depth. + */ + getFrameActor: function (frame) { + let state = this.hud.owner.getDebuggerFrames(); + if (!state) { + return null; + } + + let grip; + if (frame == this.SELECTED_FRAME) { + grip = state.frames[state.selected]; + } else { + grip = state.frames[frame]; + } + + return grip ? grip.actor : null; + }, + + /** + * Opens a new variables view that allows the inspection of the given object. + * + * @param object options + * Options for the variables view: + * - objectActor: grip of the ObjectActor you want to show in the + * variables view. + * - rawObject: the raw object you want to show in the variables view. + * - label: label to display in the variables view for inspected + * object. + * - hideFilterInput: optional boolean, |true| if you want to hide the + * variables view filter input. + * - targetElement: optional nsIDOMElement to append the variables view + * to. An iframe element is used as a container for the view. If this + * option is not used, then the variables view opens in the sidebar. + * - autofocus: optional boolean, |true| if you want to give focus to + * the variables view window after open, |false| otherwise. + * @return object + * A promise object that is resolved when the variables view has + * opened. The new variables view instance is given to the callbacks. + */ + openVariablesView: function (options) { + let onContainerReady = (window) => { + let container = window.document.querySelector("#variables"); + let view = this._variablesView; + if (!view || options.targetElement) { + let viewOptions = { + container: container, + hideFilterInput: options.hideFilterInput, + }; + view = this._createVariablesView(viewOptions); + if (!options.targetElement) { + this._variablesView = view; + window.addEventListener("keypress", this._onKeypressInVariablesView); + } + } + options.view = view; + this._updateVariablesView(options); + + if (!options.targetElement && options.autofocus) { + window.focus(); + } + + this.emit("variablesview-open", view, options); + return view; + }; + + let openPromise; + if (options.targetElement) { + let deferred = promise.defer(); + openPromise = deferred.promise; + let document = options.targetElement.ownerDocument; + let iframe = document.createElementNS(XHTML_NS, "iframe"); + + iframe.addEventListener("load", function onIframeLoad() { + 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); + options.targetElement.appendChild(iframe); + } else { + if (!this.sidebar) { + this._createSidebar(); + } + openPromise = this._addVariablesViewSidebarTab(); + } + + return openPromise.then(onContainerReady); + }, + + /** + * Create the Web Console sidebar. + * + * @see devtools/framework/sidebar.js + * @private + */ + _createSidebar: function () { + let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); + this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); + this.sidebar.show(); + this.emit("sidebar-opened"); + }, + + /** + * Add the variables view tab to the sidebar. + * + * @private + * @return object + * A promise object for the adding of the new tab. + */ + _addVariablesViewSidebarTab: function () { + let deferred = promise.defer(); + + let onTabReady = () => { + let window = this.sidebar.getWindowForTab("variablesview"); + deferred.resolve(window); + }; + + let tabPanel = this.sidebar.getTabPanel("variablesview"); + if (tabPanel) { + if (this.sidebar.getCurrentTabID() == "variablesview") { + onTabReady(); + } else { + this.sidebar.once("variablesview-selected", onTabReady); + this.sidebar.select("variablesview"); + } + } else { + this.sidebar.once("variablesview-ready", onTabReady); + this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true}); + } + + return deferred.promise; + }, + + /** + * The keypress event handler for the Variables View sidebar. Currently this + * is used for removing the sidebar when Escape is pressed. + * + * @private + * @param nsIDOMEvent event + * The keypress DOM event object. + */ + _onKeypressInVariablesView: function (event) { + let tag = event.target.nodeName; + if (event.keyCode != KeyCodes.DOM_VK_ESCAPE || event.shiftKey || + event.altKey || event.ctrlKey || event.metaKey || + ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { + return; + } + + this._sidebarDestroy(); + this.focus(); + event.stopPropagation(); + }, + + /** + * Create a variables view instance. + * + * @private + * @param object options + * Options for the new Variables View instance: + * - container: the DOM element where the variables view is inserted. + * - hideFilterInput: boolean, if true the variables filter input is + * hidden. + * @return object + * The new Variables View instance. + */ + _createVariablesView: function (options) { + let view = new VariablesView(options.container); + view.toolbox = gDevTools.getToolbox(this.hud.owner.target); + view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); + view.emptyText = l10n.getStr("emptyPropertiesList"); + view.searchEnabled = !options.hideFilterInput; + view.lazyEmpty = this._lazyVariablesView; + + VariablesViewController.attach(view, { + getEnvironmentClient: grip => { + return new EnvironmentClient(this.hud.proxy.client, grip); + }, + getObjectClient: grip => { + return new ObjectClient(this.hud.proxy.client, grip); + }, + getLongStringClient: grip => { + return this.webConsoleClient.longString(grip); + }, + releaseActor: actor => { + this.hud._releaseObject(actor); + }, + simpleValueEvalMacro: simpleValueEvalMacro, + overrideValueEvalMacro: overrideValueEvalMacro, + getterOrSetterEvalMacro: getterOrSetterEvalMacro, + }); + + // Relay events from the VariablesView. + view.on("fetched", (event, type, variableObject) => { + this.emit("variablesview-fetched", variableObject); + }); + + return view; + }, + + /** + * Update the variables view. + * + * @private + * @param object options + * Options for updating the variables view: + * - view: the view you want to update. + * - objectActor: the grip of the new ObjectActor you want to show in + * the view. + * - rawObject: the new raw object you want to show. + * - label: the new label for the inspected object. + */ + _updateVariablesView: function (options) { + let view = options.view; + view.empty(); + + // We need to avoid pruning the object inspection starting point. + // That one is pruned when the console message is removed. + view.controller.releaseActors(actor => { + return view._consoleLastObjectActor != actor; + }); + + if (options.objectActor && + (!this.hud.isBrowserConsole || + Services.prefs.getBoolPref("devtools.chrome.enabled"))) { + // Make sure eval works in the correct context. + view.eval = this._variablesViewEvaluate.bind(this, options); + view.switch = this._variablesViewSwitch.bind(this, options); + view.delete = this._variablesViewDelete.bind(this, options); + } else { + view.eval = null; + view.switch = null; + view.delete = null; + } + + let { variable, expanded } = view.controller.setSingleVariable(options); + variable.evaluationMacro = simpleValueEvalMacro; + + if (options.objectActor) { + view._consoleLastObjectActor = options.objectActor.actor; + } else if (options.rawObject) { + view._consoleLastObjectActor = null; + } else { + throw new Error( + "Variables View cannot open without giving it an object display."); + } + + expanded.then(() => { + this.emit("variablesview-updated", view, options); + }); + }, + + /** + * The evaluation function used by the variables view when editing a property + * value. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the edited property. + * @param string value + * The value the edited property was changed to. + */ + _variablesViewEvaluate: function (options, variableObject, value) { + let updater = this._updateVariablesView.bind(this, options); + let onEval = this._silentEvalCallback.bind(this, updater); + let string = variableObject.evaluationMacro(variableObject, value); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + this.requestEvaluation(string, evalOptions).then(onEval, onEval); + }, + + /** + * The property deletion function used by the variables view when a property + * is deleted. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the deleted property. + */ + _variablesViewDelete: function (options, variableObject) { + let onEval = this._silentEvalCallback.bind(this, null); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + this.requestEvaluation("delete _self" + + variableObject.symbolicName, evalOptions).then(onEval, onEval); + }, + + /** + * The property rename function used by the variables view when a property + * is renamed. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the renamed property. + * @param string newName + * The new name for the property. + */ + _variablesViewSwitch: function (options, variableObject, newName) { + let updater = this._updateVariablesView.bind(this, options); + let onEval = this._silentEvalCallback.bind(this, updater); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + let newSymbolicName = + variableObject.ownerView.symbolicName + '["' + newName + '"]'; + if (newSymbolicName == variableObject.symbolicName) { + return; + } + + let code = "_self" + newSymbolicName + " = _self" + + variableObject.symbolicName + ";" + "delete _self" + + variableObject.symbolicName; + + this.requestEvaluation(code, evalOptions).then(onEval, onEval); + }, + + /** + * A noop callback for JavaScript evaluation. This method releases any + * result ObjectActors that come from the server for evaluation requests. This + * is used for editing, renaming and deleting properties in the variables + * view. + * + * Exceptions are displayed in the output. + * + * @private + * @param function callback + * Function to invoke once the response is received. + * @param object response + * The response packet received from the server. + */ + _silentEvalCallback: function (callback, response) { + if (response.error) { + console.error("Web Console evaluation failed. " + response.error + ":" + + response.message); + + callback && callback(response); + return; + } + + if (response.exceptionMessage) { + let message = new Messages.Simple(response.exceptionMessage, { + category: "output", + severity: "error", + timestamp: response.timestamp, + }); + this.hud.output.addMessage(message); + message._objectActors = new Set(); + if (WebConsoleUtils.isActorGrip(response.exception)) { + message._objectActors.add(response.exception.actor); + } + } + + let helper = response.helperResult || { type: null }; + let helperGrip = null; + if (helper.type == "inspectObject") { + helperGrip = helper.object; + } + + let grips = [response.result, helperGrip]; + for (let grip of grips) { + if (WebConsoleUtils.isActorGrip(grip)) { + this.hud._releaseObject(grip.actor); + } + } + + callback && callback(response); + }, + + /** + * Clear the Web Console output. + * + * This method emits the "messages-cleared" notification. + * + * @param boolean clearStorage + * True if you want to clear the console messages storage associated to + * this Web Console. + */ + clearOutput: function (clearStorage) { + let hud = this.hud; + let outputNode = hud.outputNode; + let node; + while ((node = outputNode.firstChild)) { + hud.removeOutputMessage(node); + } + + hud.groupDepth = 0; + hud._outputQueue.forEach(hud._destroyItem, hud); + hud._outputQueue = []; + this.webConsoleClient.clearNetworkRequests(); + hud._repeatNodes = {}; + + if (clearStorage) { + this.webConsoleClient.clearMessagesCache(); + } + + this._sidebarDestroy(); + + if (hud.NEW_CONSOLE_OUTPUT_ENABLED) { + hud.newConsoleOutput.dispatchMessagesClear(); + } + + this.emit("messages-cleared"); + }, + + /** + * Remove all of the private messages from the Web Console output. + * + * This method emits the "private-messages-cleared" notification. + */ + clearPrivateMessages: function () { + let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); + for (let node of nodes) { + this.hud.removeOutputMessage(node); + } + this.emit("private-messages-cleared"); + }, + + /** + * Updates the size of the input field (command line) to fit its contents. + * + * @returns void + */ + resizeInput: function () { + let inputNode = this.inputNode; + + // Reset the height so that scrollHeight will reflect the natural height of + // the contents of the input field. + inputNode.style.height = "auto"; + + // Now resize the input field to fit its contents. + let scrollHeight = inputNode.inputField.scrollHeight; + if (scrollHeight > 0) { + inputNode.style.height = scrollHeight + "px"; + } + }, + + /** + * Sets the value of the input field (command line), and resizes the field to + * fit its contents. This method is preferred over setting "inputNode.value" + * directly, because it correctly resizes the field. + * + * @param string newValue + * The new value to set. + * @returns void + */ + setInputValue: function (newValue) { + this.inputNode.value = newValue; + this.lastInputValue = newValue; + this.completeNode.value = ""; + this.resizeInput(); + this._inputChanged = true; + this.emit("set-input-value"); + }, + + /** + * Gets the value from the input field + * @returns string + */ + getInputValue: function () { + return this.inputNode.value || ""; + }, + + /** + * The inputNode "input" and "keyup" event handler. + * @private + */ + _inputEventHandler: function () { + if (this.lastInputValue != this.getInputValue()) { + this.resizeInput(); + this.complete(this.COMPLETE_HINT_ONLY); + this.lastInputValue = this.getInputValue(); + this._inputChanged = true; + } + }, + + /** + * The window "blur" event handler. + * @private + */ + _blurEventHandler: function () { + if (this.autocompletePopup) { + this.clearCompletion(); + } + }, + + /* eslint-disable complexity */ + /** + * The inputNode "keypress" event handler. + * + * @private + * @param nsIDOMEvent event + */ + _keyPress: function (event) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + let inputUpdated = false; + + if (event.ctrlKey) { + switch (event.charCode) { + case 101: + // control-e + if (Services.appinfo.OS == "WINNT") { + break; + } + let lineEndPos = inputValue.length; + if (this.hasMultilineInput()) { + // find index of closest newline >= cursor + for (let i = inputNode.selectionEnd; i < lineEndPos; i++) { + if (inputValue.charAt(i) == "\r" || + inputValue.charAt(i) == "\n") { + lineEndPos = i; + break; + } + } + } + inputNode.setSelectionRange(lineEndPos, lineEndPos); + event.preventDefault(); + this.clearCompletion(); + break; + + case 110: + // Control-N differs from down arrow: it ignores autocomplete state. + // Note that we preserve the default 'down' navigation within + // multiline text. + if (Services.appinfo.OS == "Darwin" && + this.canCaretGoNext() && + this.historyPeruse(HISTORY_FORWARD)) { + event.preventDefault(); + // Ctrl-N is also used to focus the Network category button on + // MacOSX. The preventDefault() call doesn't prevent the focus + // from moving away from the input. + this.focus(); + } + this.clearCompletion(); + break; + + case 112: + // Control-P differs from up arrow: it ignores autocomplete state. + // Note that we preserve the default 'up' navigation within + // multiline text. + if (Services.appinfo.OS == "Darwin" && + this.canCaretGoPrevious() && + this.historyPeruse(HISTORY_BACK)) { + event.preventDefault(); + // Ctrl-P may also be used to focus some category button on MacOSX. + // The preventDefault() call doesn't prevent the focus from moving + // away from the input. + this.focus(); + } + this.clearCompletion(); + break; + default: + break; + } + return; + } else if (event.keyCode == KeyCodes.DOM_VK_RETURN) { + let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE); + if (event.shiftKey || + (!Debugger.isCompilableUnit(inputNode.value) && autoMultiline)) { + // shift return or incomplete statement + return; + } + } + + switch (event.keyCode) { + case KeyCodes.DOM_VK_ESCAPE: + if (this.autocompletePopup.isOpen) { + this.clearCompletion(); + event.preventDefault(); + event.stopPropagation(); + } else if (this.sidebar) { + this._sidebarDestroy(); + event.preventDefault(); + event.stopPropagation(); + } + break; + + case KeyCodes.DOM_VK_RETURN: + if (this._autocompletePopupNavigated && + this.autocompletePopup.isOpen && + this.autocompletePopup.selectedIndex > -1) { + this.acceptProposedCompletion(); + } else { + this.execute(); + this._inputChanged = false; + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_BACKWARD); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else if (this.canCaretGoPrevious()) { + inputUpdated = this.historyPeruse(HISTORY_BACK); + } + if (inputUpdated) { + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_FORWARD); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else if (this.canCaretGoNext()) { + inputUpdated = this.historyPeruse(HISTORY_FORWARD); + } + if (inputUpdated) { + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_PAGE_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEUP); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else { + this.hud.outputScroller.scrollTop = + Math.max(0, + this.hud.outputScroller.scrollTop - + this.hud.outputScroller.clientHeight + ); + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_PAGE_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else { + this.hud.outputScroller.scrollTop = + Math.min(this.hud.outputScroller.scrollHeight, + this.hud.outputScroller.scrollTop + + this.hud.outputScroller.clientHeight + ); + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_HOME: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = 0; + event.preventDefault(); + } else if (inputValue.length <= 0) { + this.hud.outputScroller.scrollTop = 0; + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_END: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = + this.autocompletePopup.itemCount - 1; + event.preventDefault(); + } else if (inputValue.length <= 0) { + this.hud.outputScroller.scrollTop = + this.hud.outputScroller.scrollHeight; + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_LEFT: + if (this.autocompletePopup.isOpen || this.lastCompletion.value) { + this.clearCompletion(); + } + break; + + case KeyCodes.DOM_VK_RIGHT: + let cursorAtTheEnd = this.inputNode.selectionStart == + this.inputNode.selectionEnd && + this.inputNode.selectionStart == + inputValue.length; + let haveSuggestion = this.autocompletePopup.isOpen || + this.lastCompletion.value; + let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated; + if (haveSuggestion && useCompletion && + this.complete(this.COMPLETE_HINT_ONLY) && + this.lastCompletion.value && + this.acceptProposedCompletion()) { + event.preventDefault(); + } + if (this.autocompletePopup.isOpen) { + this.clearCompletion(); + } + break; + + case KeyCodes.DOM_VK_TAB: + // Generate a completion and accept the first proposed value. + if (this.complete(this.COMPLETE_HINT_ONLY) && + this.lastCompletion && + this.acceptProposedCompletion()) { + event.preventDefault(); + } else if (this._inputChanged) { + this.updateCompleteNode(l10n.getStr("Autocomplete.blank")); + event.preventDefault(); + } + break; + default: + break; + } + }, + /* eslint-enable complexity */ + + /** + * The inputNode "focus" event handler. + * @private + */ + _focusEventHandler: function () { + this._inputChanged = false; + }, + + /** + * Go up/down the history stack of input values. + * + * @param number direction + * History navigation direction: HISTORY_BACK or HISTORY_FORWARD. + * + * @returns boolean + * True if the input value changed, false otherwise. + */ + historyPeruse: function (direction) { + if (!this.history.length) { + return false; + } + + // Up Arrow key + if (direction == HISTORY_BACK) { + if (this.historyPlaceHolder <= 0) { + return false; + } + let inputVal = this.history[--this.historyPlaceHolder]; + + // Save the current input value as the latest entry in history, only if + // the user is already at the last entry. + // Note: this code does not store changes to items that are already in + // history. + if (this.historyPlaceHolder + 1 == this.historyIndex) { + this.history[this.historyIndex] = this.getInputValue() || ""; + } + + this.setInputValue(inputVal); + } else if (direction == HISTORY_FORWARD) { + // Down Arrow key + if (this.historyPlaceHolder >= (this.history.length - 1)) { + return false; + } + + let inputVal = this.history[++this.historyPlaceHolder]; + this.setInputValue(inputVal); + } else { + throw new Error("Invalid argument 0"); + } + + return true; + }, + + /** + * Test for multiline input. + * + * @return boolean + * True if CR or LF found in node value; else false. + */ + hasMultilineInput: function () { + return /[\r\n]/.test(this.getInputValue()); + }, + + /** + * Check if the caret is at a location that allows selecting the previous item + * in history when the user presses the Up arrow key. + * + * @return boolean + * True if the caret is at a location that allows selecting the + * previous item in history when the user presses the Up arrow key, + * otherwise false. + */ + canCaretGoPrevious: function () { + let node = this.inputNode; + if (node.selectionStart != node.selectionEnd) { + return false; + } + + let multiline = /[\r\n]/.test(node.value); + return node.selectionStart == 0 ? true : + node.selectionStart == node.value.length && !multiline; + }, + + /** + * Check if the caret is at a location that allows selecting the next item in + * history when the user presses the Down arrow key. + * + * @return boolean + * True if the caret is at a location that allows selecting the next + * item in history when the user presses the Down arrow key, otherwise + * false. + */ + canCaretGoNext: function () { + let node = this.inputNode; + if (node.selectionStart != node.selectionEnd) { + return false; + } + + let multiline = /[\r\n]/.test(node.value); + return node.selectionStart == node.value.length ? true : + node.selectionStart == 0 && !multiline; + }, + + /** + * Completes the current typed text in the inputNode. Completion is performed + * only if the selection/cursor is at the end of the string. If no completion + * is found, the current inputNode value and cursor/selection stay. + * + * @param int type possible values are + * - this.COMPLETE_FORWARD: 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 next completion of all possible + * completions is used. If the value changed, then the first possible + * completion is used and the selection is set from the current + * cursor position to the end of the completed text. + * If there is only one possible completion, then this completion + * value is used and the cursor is put at the end of the completion. + * - 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 + * used again. If there is only one possible completion, then + * the this.getInputValue() is set to this value and the selection + * is set from the current cursor position to the end of the + * completed text. + * @param function callback + * Optional function invoked when the autocomplete properties are + * updated. + * @returns boolean true if there existed a completion for the current input, + * or false otherwise. + */ + complete: function (type, callback) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + 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(); + callback && callback(this); + this.emit("autocomplete-updated"); + return false; + } + + // Only complete if the selection is empty. + if (inputNode.selectionStart != inputNode.selectionEnd) { + this.clearCompletion(); + callback && callback(this); + this.emit("autocomplete-updated"); + return false; + } + + // Update the completion results. + if (this.lastCompletion.value != inputValue || + frameActor != this._lastFrameActorId) { + this._updateCompletionResult(type, callback); + return false; + } + + let popup = this.autocompletePopup; + let accepted = false; + + if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + this.acceptProposedCompletion(); + accepted = true; + } else if (type == this.COMPLETE_BACKWARD) { + popup.selectPreviousItem(); + } else if (type == this.COMPLETE_FORWARD) { + popup.selectNextItem(); + } else if (type == this.COMPLETE_PAGEUP) { + popup.selectPreviousPageItem(); + } else if (type == this.COMPLETE_PAGEDOWN) { + popup.selectNextPageItem(); + } + + callback && callback(this); + this.emit("autocomplete-updated"); + return accepted || popup.itemCount > 0; + }, + + /** + * Update the completion result. This operation is performed asynchronously by + * fetching updated results from the content process. + * + * @private + * @param int type + * Completion type. See this.complete() for details. + * @param function [callback] + * Optional, function to invoke when completion results are received. + */ + _updateCompletionResult: function (type, callback) { + let frameActor = this.getFrameActor(this.SELECTED_FRAME); + if (this.lastCompletion.value == this.getInputValue() && + frameActor == this._lastFrameActorId) { + return; + } + + let requestId = gSequenceId(); + let cursor = this.inputNode.selectionStart; + let input = this.getInputValue().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: type, + value: null, + }; + + let response = { matches: newList, matchProp: filterBy }; + this._receiveAutocompleteProperties(null, callback, response); + return; + } + + this._lastFrameActorId = frameActor; + + this.lastCompletion = { + requestId: requestId, + completionType: type, + value: null, + }; + + let autocompleteCallback = + this._receiveAutocompleteProperties.bind(this, requestId, callback); + + this.webConsoleClient.autocomplete( + input, cursor, autocompleteCallback, frameActor); + }, + + /** + * Handler for the autocompletion results. This method takes + * the completion result received from the server and updates the UI + * accordingly. + * + * @param number requestId + * Request ID. + * @param function [callback=null] + * Optional, function to invoke when the completion result is received. + * @param object message + * The JSON message which holds the completion results received from + * the content process. + */ + _receiveAutocompleteProperties: function (requestId, callback, message) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + if (this.lastCompletion.value == inputValue || + requestId != 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 (requestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { + this._autocompleteCache = message.matches; + this._autocompleteQuery = inputUntilCursor; + } + + let matches = message.matches; + let lastPart = message.matchProp; + if (!matches.length) { + this.clearCompletion(); + callback && callback(this); + this.emit("autocomplete-updated"); + return; + } + + let items = matches.reverse().map(function (match) { + return { preLabel: lastPart, label: match }; + }); + + let popup = this.autocompletePopup; + popup.setItems(items); + + let completionType = this.lastCompletion.completionType; + this.lastCompletion = { + value: inputValue, + matchProp: lastPart, + }; + + if (items.length > 1 && !popup.isOpen) { + let str = this.getInputValue().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) { + popup.hidePopup(); + this._autocompletePopupNavigated = false; + } + + if (items.length == 1) { + popup.selectedIndex = 0; + } + + this.onAutocompleteSelect(); + + if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + this.acceptProposedCompletion(); + } else if (completionType == this.COMPLETE_BACKWARD) { + popup.selectPreviousItem(); + } else if (completionType == this.COMPLETE_FORWARD) { + popup.selectNextItem(); + } + + callback && callback(this); + this.emit("autocomplete-updated"); + }, + + onAutocompleteSelect: function () { + // Render the suggestion only if the cursor is at the end of the input. + if (this.inputNode.selectionStart != this.getInputValue().length) { + return; + } + + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion.value) { + let suffix = + currentItem.label.substring(this.lastCompletion.matchProp.length); + this.updateCompleteNode(suffix); + } else { + this.updateCompleteNode(""); + } + }, + + /** + * Clear the current completion information and close the autocomplete popup, + * if needed. + */ + clearCompletion: function () { + this.autocompletePopup.clearItems(); + this.lastCompletion = { value: null }; + this.updateCompleteNode(""); + if (this.autocompletePopup.isOpen) { + // Trigger a blur/focus of the JSTerm input to force screen readers to read the + // value again. + this.inputNode.blur(); + this.autocompletePopup.once("popup-closed", () => { + this.inputNode.focus(); + }); + this.autocompletePopup.hidePopup(); + this._autocompletePopupNavigated = false; + } + }, + + /** + * Accept the proposed input completion. + * + * @return boolean + * True if there was a selected completion item and the input value + * was updated, false otherwise. + */ + acceptProposedCompletion: function () { + let updated = false; + + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion.value) { + let suffix = + currentItem.label.substring(this.lastCompletion.matchProp.length); + let cursor = this.inputNode.selectionStart; + let value = this.getInputValue(); + this.setInputValue(value.substr(0, cursor) + + suffix + value.substr(cursor)); + let newCursor = cursor + suffix.length; + this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; + updated = true; + } + + this.clearCompletion(); + + return updated; + }, + + /** + * Update the node that displays the currently selected autocomplete proposal. + * + * @param string suffix + * The proposed suffix for the inputNode value. + */ + updateCompleteNode: function (suffix) { + // completion prefix = input, with non-control chars replaced by spaces + let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : ""; + this.completeNode.value = prefix + suffix; + }, + + /** + * Destroy the sidebar. + * @private + */ + _sidebarDestroy: function () { + if (this._variablesView) { + this._variablesView.controller.releaseActors(); + this._variablesView = null; + } + + if (this.sidebar) { + this.sidebar.hide(); + this.sidebar.destroy(); + this.sidebar = null; + } + + this.emit("sidebar-closed"); + }, + + /** + * Destroy the JSTerm object. Call this method to avoid memory leaks. + */ + destroy: function () { + this._sidebarDestroy(); + + this.clearCompletion(); + this.clearOutput(); + + this.autocompletePopup.destroy(); + this.autocompletePopup = null; + + 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; + }, +}; + +function gSequenceId() { + return gSequenceId.n++; +} +gSequenceId.n = 0; +exports.gSequenceId = gSequenceId; + +/** + * @see VariablesView.simpleValueEvalMacro + */ +function simpleValueEvalMacro(item, currentString) { + return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); +} + +/** + * @see VariablesView.overrideValueEvalMacro + */ +function overrideValueEvalMacro(item, currentString) { + return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); +} + +/** + * @see VariablesView.getterOrSetterEvalMacro + */ +function getterOrSetterEvalMacro(item, currentString) { + return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); +} + +exports.JSTerm = JSTerm; diff --git a/devtools/client/webconsole/moz.build b/devtools/client/webconsole/moz.build new file mode 100644 index 0000000000..c8324b315e --- /dev/null +++ b/devtools/client/webconsole/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +DIRS += [ + 'net', + 'new-console-output', +] + +DevToolsModules( + 'console-commands.js', + 'console-output.js', + 'hudservice.js', + 'jsterm.js', + 'panel.js', + 'utils.js', + 'webconsole.js', +) diff --git a/devtools/client/webconsole/net/.eslintrc.js b/devtools/client/webconsole/net/.eslintrc.js new file mode 100644 index 0000000000..e105ac6e2a --- /dev/null +++ b/devtools/client/webconsole/net/.eslintrc.js @@ -0,0 +1,20 @@ +"use strict"; + +module.exports = { + "globals": { + "Locale": true, + "Document": true, + "document": true, + "Node": true, + "Element": true, + "MessageEvent": true, + "BrowserLoader": true, + "addEventListener": true, + "DOMParser": true, + "dispatchEvent": true, + "setTimeout": true + }, + "rules": { + "no-unused-vars": ["error", {"args": "none"}], + } +}; diff --git a/devtools/client/webconsole/net/components/cookies-tab.js b/devtools/client/webconsole/net/components/cookies-tab.js new file mode 100644 index 0000000000..d76414679c --- /dev/null +++ b/devtools/client/webconsole/net/components/cookies-tab.js @@ -0,0 +1,75 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Cookies' tab displayed when the user + * expands network log in the Console panel. It's responsible for rendering + * sent and received cookies. + */ +var CookiesTab = React.createClass({ + propTypes: { + actions: PropTypes.shape({ + requestData: PropTypes.func.isRequired + }), + data: PropTypes.object.isRequired, + }, + + displayName: "CookiesTab", + + componentDidMount() { + let { actions, data } = this.props; + let requestCookies = data.request.cookies; + let responseCookies = data.response.cookies; + + // TODO: use async action objects as soon as Redux is in place + if (!requestCookies || !requestCookies.length) { + actions.requestData("requestCookies"); + } + + if (!responseCookies || !responseCookies.length) { + actions.requestData("responseCookies"); + } + }, + + render() { + let { actions, data: file } = this.props; + let requestCookies = file.request.cookies; + let responseCookies = file.response.cookies; + + // The cookie panel displays two groups of cookies: + // 1) Response Cookies + // 2) Request Cookies + let groups = [{ + key: "responseCookies", + name: Locale.$STR("responseCookies"), + params: responseCookies + }, { + key: "requestCookies", + name: Locale.$STR("requestCookies"), + params: requestCookies + }]; + + return ( + DOM.div({className: "cookiesTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = CookiesTab; diff --git a/devtools/client/webconsole/net/components/headers-tab.js b/devtools/client/webconsole/net/components/headers-tab.js new file mode 100644 index 0000000000..2eca3fd2fd --- /dev/null +++ b/devtools/client/webconsole/net/components/headers-tab.js @@ -0,0 +1,79 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Headers' tab displayed when the user + * expands network log in the Console panel. It's responsible for rendering + * request and response HTTP headers. + */ +var HeadersTab = React.createClass({ + propTypes: { + actions: PropTypes.shape({ + requestData: PropTypes.func.isRequired + }), + data: PropTypes.object.isRequired, + }, + + displayName: "HeadersTab", + + componentDidMount() { + let { actions, data } = this.props; + let requestHeaders = data.request.headers; + let responseHeaders = data.response.headers; + + // Request headers if they are not available yet. + // TODO: use async action objects as soon as Redux is in place + if (!requestHeaders) { + actions.requestData("requestHeaders"); + } + + if (!responseHeaders) { + actions.requestData("responseHeaders"); + } + }, + + render() { + let { data } = this.props; + let requestHeaders = data.request.headers; + let responseHeaders = data.response.headers; + + // TODO: Another groups to implement: + // 1) Cached Headers + // 2) Headers from upload stream + let groups = [{ + key: "responseHeaders", + name: Locale.$STR("responseHeaders"), + params: responseHeaders + }, { + key: "requestHeaders", + name: Locale.$STR("requestHeaders"), + params: requestHeaders + }]; + + // If response headers are not available yet, display a spinner + if (!responseHeaders || !responseHeaders.length) { + groups[0].content = Spinner(); + } + + return ( + DOM.div({className: "headersTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({groups: groups}) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = HeadersTab; diff --git a/devtools/client/webconsole/net/components/moz.build b/devtools/client/webconsole/net/components/moz.build new file mode 100644 index 0000000000..0053de7807 --- /dev/null +++ b/devtools/client/webconsole/net/components/moz.build @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'cookies-tab.js', + 'headers-tab.js', + 'net-info-body.css', + 'net-info-body.js', + 'net-info-group-list.js', + 'net-info-group.css', + 'net-info-group.js', + 'net-info-params.css', + 'net-info-params.js', + 'params-tab.js', + 'post-tab.js', + 'response-tab.css', + 'response-tab.js', + 'size-limit.css', + 'size-limit.js', + 'spinner.js', + 'stacktrace-tab.js', +) diff --git a/devtools/client/webconsole/net/components/net-info-body.css b/devtools/client/webconsole/net/components/net-info-body.css new file mode 100644 index 0000000000..2d0bac70e2 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-body.css @@ -0,0 +1,112 @@ +/* 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/. */ + +/******************************************************************************/ +/* Network Info Body */ + +.netInfoBody { + margin: 10px 0 0 0; + width: 100%; + cursor: default; + display: block; +} + +.netInfoBody *:focus { + outline: 0 !important; +} + +.netInfoBody .panelContent { + word-break: break-all; +} + +/******************************************************************************/ +/* Network Info Body Tabs */ + +.netInfoBody > .tabs { + background-color: transparent; + background-image: none; + height: 100%; +} + +.netInfoBody > .tabs .tabs-navigation { + border-bottom-color: var(--net-border); + background-color: transparent; + text-decoration: none; + padding-top: 3px; + padding-left: 7px; + padding-bottom: 1px; + border-bottom: 1px solid var(--net-border); +} + +.netInfoBody > .tabs .tabs-menu { + display: table; + list-style: none; + padding: 0; + margin: 0; +} + +/* This is the trick that makes the tab bottom border invisible */ +.netInfoBody > .tabs .tabs-menu-item { + position: relative; + bottom: -2px; + float: left; +} + +.netInfoBody > .tabs .tabs-menu-item a { + display: block; + border: 1px solid transparent; + text-decoration: none; + padding: 5px 8px 4px 8px;; + font-weight: bold; + color: var(--theme-body-color); + border-radius: 4px 4px 0 0; +} + +.netInfoBody > .tabs .tab-panel { + background-color: var(--theme-body-background); + border: 1px solid transparent; + border-top: none; + padding: 10px; + overflow: auto; + height: calc(100% - 31px); /* minus the height of the tab bar */ +} + +.netInfoBody > .tabs .tab-panel > div, +.netInfoBody > .tabs .tab-panel > div > div { + height: 100%; +} + +.netInfoBody > .tabs .tabs-menu-item.is-active a, +.netInfoBody > .tabs .tabs-menu-item.is-active a:focus, +.netInfoBody > .tabs .tabs-menu-item.is-active:hover a { + background-color: var(--theme-body-background); + border: 1px solid transparent; + border-bottom-color: var(--theme-highlight-bluegrey); + color: var(--theme-highlight-bluegrey); +} + +.netInfoBody > .tabs .tabs-menu-item:hover a { + border: 1px solid transparent; + border-bottom: 1px solid var(--net-border); + background-color: var(--theme-body-background); +} + + +/******************************************************************************/ +/* Themes */ + +.theme-firebug .netInfoBody > .tabs .tab-panel { + border-color: var(--net-border); +} + +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a, +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active:hover a, +.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a:focus { + border: 1px solid var(--net-border); + border-bottom-color: transparent; +} + +.theme-firebug .netInfoBody > .tabs .tabs-menu-item:hover a { + border-bottom-color: transparent; +} diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js new file mode 100644 index 0000000000..c5eccd458b --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-body.js @@ -0,0 +1,179 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs")); + +// Network +const HeadersTab = React.createFactory(require("./headers-tab")); +const ResponseTab = React.createFactory(require("./response-tab")); +const ParamsTab = React.createFactory(require("./params-tab")); +const CookiesTab = React.createFactory(require("./cookies-tab")); +const PostTab = React.createFactory(require("./post-tab")); +const StackTraceTab = React.createFactory(require("./stacktrace-tab")); +const NetUtils = require("../utils/net"); + +// Shortcuts +const PropTypes = React.PropTypes; + +/** + * This template renders the basic Network log info body. It's not + * visible by default, the user needs to expand the network log + * to see it. + * + * This is the set of tabs displaying details about network events: + * 1) Headers - request and response headers + * 2) Params - URL parameters + * 3) Response - response body + * 4) Cookies - request and response cookies + * 5) Post - posted data + */ +var NetInfoBody = React.createClass({ + propTypes: { + tabActive: PropTypes.number.isRequired, + actions: PropTypes.object.isRequired, + data: PropTypes.shape({ + request: PropTypes.object.isRequired, + response: PropTypes.object.isRequired + }) + }, + + displayName: "NetInfoBody", + + getDefaultProps() { + return { + tabActive: 0 + }; + }, + + getInitialState() { + return { + data: { + request: {}, + response: {} + }, + tabActive: this.props.tabActive, + }; + }, + + onTabChanged(index) { + this.setState({tabActive: index}); + }, + + hasCookies() { + let {request, response} = this.state.data; + return this.state.hasCookies || + NetUtils.getHeaderValue(request.headers, "Cookie") || + NetUtils.getHeaderValue(response.headers, "Set-Cookie"); + }, + + hasStackTrace() { + let {cause} = this.state.data; + return cause && cause.stacktrace && cause.stacktrace.length > 0; + }, + + getTabPanels() { + let actions = this.props.actions; + let data = this.state.data; + let {request} = data; + + // Flags for optional tabs. Some tabs are visible only if there + // are data to display. + let hasParams = request.queryString && request.queryString.length; + let hasPostData = request.bodySize > 0; + + let panels = []; + + // Headers tab + panels.push( + TabPanel({ + className: "headers", + key: "headers", + title: Locale.$STR("netRequest.headers")}, + HeadersTab({data: data, actions: actions}) + ) + ); + + // URL parameters tab + if (hasParams) { + panels.push( + TabPanel({ + className: "params", + key: "params", + title: Locale.$STR("netRequest.params")}, + ParamsTab({data: data, actions: actions}) + ) + ); + } + + // Posted data tab + if (hasPostData) { + panels.push( + TabPanel({ + className: "post", + key: "post", + title: Locale.$STR("netRequest.post")}, + PostTab({data: data, actions: actions}) + ) + ); + } + + // Response tab + panels.push( + TabPanel({className: "response", key: "response", + title: Locale.$STR("netRequest.response")}, + ResponseTab({data: data, actions: actions}) + ) + ); + + // Cookies tab + if (this.hasCookies()) { + panels.push( + TabPanel({ + className: "cookies", + key: "cookies", + title: Locale.$STR("netRequest.cookies")}, + CookiesTab({ + data: data, + actions: actions + }) + ) + ); + } + + // Stacktrace tab + if (this.hasStackTrace()) { + panels.push( + TabPanel({ + className: "stacktrace-tab", + key: "stacktrace", + title: Locale.$STR("netRequest.callstack")}, + StackTraceTab({ + data: data, + actions: actions + }) + ) + ); + } + + return panels; + }, + + render() { + let tabActive = this.state.tabActive; + let tabPanels = this.getTabPanels(); + return ( + Tabs({ + tabActive: tabActive, + onAfterChange: this.onTabChanged}, + tabPanels + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoBody; diff --git a/devtools/client/webconsole/net/components/net-info-group-list.js b/devtools/client/webconsole/net/components/net-info-group-list.js new file mode 100644 index 0000000000..247a23bb7c --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group-list.js @@ -0,0 +1,47 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); +const NetInfoGroup = React.createFactory(require("./net-info-group")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template is responsible for rendering sections/groups inside tabs. + * It's used e.g to display Response and Request headers as separate groups. + */ +var NetInfoGroupList = React.createClass({ + propTypes: { + groups: PropTypes.array.isRequired, + }, + + displayName: "NetInfoGroupList", + + render() { + let groups = this.props.groups; + + // Filter out empty groups. + groups = groups.filter(group => { + return group && ((group.params && group.params.length) || group.content); + }); + + // Render groups + groups = groups.map(group => { + group.type = group.key; + return NetInfoGroup(group); + }); + + return ( + DOM.div({className: "netInfoGroupList"}, + groups + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoGroupList; diff --git a/devtools/client/webconsole/net/components/net-info-group.css b/devtools/client/webconsole/net/components/net-info-group.css new file mode 100644 index 0000000000..43800019f0 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group.css @@ -0,0 +1,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/. */ + +/******************************************************************************/ +/* Net Info Group */ + +.netInfoBody .netInfoGroup { + padding-bottom: 6px; +} + +/* Last group doesn't need bottom padding */ +.netInfoBody .netInfoGroup:last-child { + padding-bottom: 0; +} + +.netInfoBody .netInfoGroup:last-child .netInfoGroupContent { + padding-bottom: 0; +} + +.netInfoBody .netInfoGroupTitle { + cursor: pointer; + font-weight: bold; + -moz-user-select: none; + cursor: pointer; + padding-left: 3px; +} + +.netInfoBody .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/controls.png"); + background-size: 56px 28px; + background-position: 0 -14px; + background-repeat: no-repeat; + width: 14px; + height: 14px; + cursor: pointer; + display: inline-block; + vertical-align: middle; +} + +.netInfoBody .netInfoGroup.opened .netInfoGroupTwisty { + background-position: -14px -14px; +} + +/* Group content is expandable/collapsible by clicking on the title */ +.netInfoBody .netInfoGroupContent { + padding-top: 7px; + margin-top: 3px; + padding-bottom: 14px; + border-top: 1px solid var(--net-border); + display: none; +} + +/* Toggle group visibility */ +.netInfoBody .netInfoGroup.opened .netInfoGroupContent { + display: block; +} + +/******************************************************************************/ +/* Themes */ + +.theme-dark .netInfoBody .netInfoGroup { + color: var(--theme-body-color); +} + +.theme-dark .netInfoBody .netInfoGroup .netInfoGroupTwisty { + filter: invert(1); +} + +/* Twisties */ +.theme-firebug .netInfoBody .netInfoGroup .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg"); + background-position: 0 2px; + background-size: 11px 11px; + width: 15px; +} + +.theme-firebug .netInfoBody .netInfoGroup.opened .netInfoGroupTwisty { + background-image: url("chrome://devtools/skin/images/firebug/twisty-open-firebug.svg"); +} diff --git a/devtools/client/webconsole/net/components/net-info-group.js b/devtools/client/webconsole/net/components/net-info-group.js new file mode 100644 index 0000000000..d9794652e0 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-group.js @@ -0,0 +1,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 React = require("devtools/client/shared/vendor/react"); +const NetInfoParams = React.createFactory(require("./net-info-params")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents a group of data within a tab. For example, + * Headers tab has two groups 'Request Headers' and 'Response Headers' + * The Response tab can also have two groups 'Raw Data' and 'JSON' + */ +var NetInfoGroup = React.createClass({ + propTypes: { + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + params: PropTypes.array, + content: PropTypes.element, + open: PropTypes.bool + }, + + displayName: "NetInfoGroup", + + getDefaultProps() { + return { + open: true, + }; + }, + + getInitialState() { + return { + open: this.props.open, + }; + }, + + onToggle(event) { + this.setState({ + open: !this.state.open + }); + }, + + render() { + let content = this.props.content; + + if (!content && this.props.params) { + content = NetInfoParams({ + params: this.props.params + }); + } + + let open = this.state.open; + let className = open ? "opened" : ""; + + return ( + DOM.div({className: "netInfoGroup" + " " + className + " " + + this.props.type}, + DOM.span({ + className: "netInfoGroupTwisty", + onClick: this.onToggle + }), + DOM.span({ + className: "netInfoGroupTitle", + onClick: this.onToggle}, + this.props.name + ), + DOM.div({className: "netInfoGroupContent"}, + content + ) + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoGroup; diff --git a/devtools/client/webconsole/net/components/net-info-params.css b/devtools/client/webconsole/net/components/net-info-params.css new file mode 100644 index 0000000000..4ec7140f80 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-params.css @@ -0,0 +1,23 @@ +/* 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/. */ + +/******************************************************************************/ +/* Net Info Params */ + +.netInfoBody .netInfoParamName { + padding: 0 10px 0 0; + font-weight: bold; + vertical-align: top; + text-align: right; + white-space: nowrap; +} + +.netInfoBody .netInfoParamValue { + width: 100%; + word-wrap: break-word; +} + +.netInfoBody .netInfoParamValue > code { + font-family: var(--monospace-font-family); +} diff --git a/devtools/client/webconsole/net/components/net-info-params.js b/devtools/client/webconsole/net/components/net-info-params.js new file mode 100644 index 0000000000..573257b281 --- /dev/null +++ b/devtools/client/webconsole/net/components/net-info-params.js @@ -0,0 +1,58 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template renders list of parameters within a group. + * It's essentially a list of name + value pairs. + */ +var NetInfoParams = React.createClass({ + displayName: "NetInfoParams", + + propTypes: { + params: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + })).isRequired, + }, + + render() { + let params = this.props.params || []; + + params.sort(function (a, b) { + return a.name > b.name ? 1 : -1; + }); + + let rows = []; + params.forEach((param, index) => { + rows.push( + DOM.tr({key: index}, + DOM.td({className: "netInfoParamName"}, + DOM.span({title: param.name}, param.name) + ), + DOM.td({className: "netInfoParamValue"}, + DOM.code({}, param.value) + ) + ) + ); + }); + + return ( + DOM.table({cellPadding: 0, cellSpacing: 0}, + DOM.tbody({}, + rows + ) + ) + ); + } +}); + +// Exports from this module +module.exports = NetInfoParams; diff --git a/devtools/client/webconsole/net/components/params-tab.js b/devtools/client/webconsole/net/components/params-tab.js new file mode 100644 index 0000000000..c3fefc6699 --- /dev/null +++ b/devtools/client/webconsole/net/components/params-tab.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"; + +const React = require("devtools/client/shared/vendor/react"); +const NetInfoParams = React.createFactory(require("./net-info-params")); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Params' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * displaying URL parameters (query string). + */ +var ParamsTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired + }) + }, + + displayName: "ParamsTab", + + render() { + let data = this.props.data; + + return ( + DOM.div({className: "paramsTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoParams({params: data.request.queryString}) + ) + ) + ); + } +}); + +// Exports from this module +module.exports = ParamsTab; diff --git a/devtools/client/webconsole/net/components/post-tab.js b/devtools/client/webconsole/net/components/post-tab.js new file mode 100644 index 0000000000..6d06eb40bd --- /dev/null +++ b/devtools/client/webconsole/net/components/post-tab.js @@ -0,0 +1,279 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Reps +const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils"); +const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view")); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); + +// Network +const NetInfoParams = React.createFactory(require("./net-info-params")); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); +const SizeLimit = React.createFactory(require("./size-limit")); +const NetUtils = require("../utils/net"); +const Json = require("../utils/json"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Post' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * displaying posted data (HTTP post body). + */ +var PostTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired + }), + actions: PropTypes.object.isRequired + }, + + displayName: "PostTab", + + isJson(file) { + let text = file.request.postData.text; + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + return Json.isJSON(value, text); + }, + + parseJson(file) { + let postData = file.request.postData; + if (!postData) { + return null; + } + + let jsonString = new String(postData.text); + return Json.parseJSONString(jsonString); + }, + + /** + * Render JSON post data as an expandable tree. + */ + renderJson(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!this.isJson(file)) { + return null; + } + + let json = this.parseJson(file); + if (!json) { + return null; + } + + return { + key: "json", + content: TreeView({ + columns: [{id: "value"}], + object: json, + mode: "tiny", + renderValue: props => Rep(Object.assign({}, props, { + cropLimit: 50, + })), + }), + name: Locale.$STR("jsonScopeName") + }; + }, + + parseXml(file) { + let text = file.request.postData.text; + if (isLongString(text)) { + return null; + } + + return NetUtils.parseXml({ + mimeType: NetUtils.getHeaderValue(file.request.headers, "content-type"), + text: text, + }); + }, + + isXml(file) { + if (isLongString(file.request.postData.text)) { + return false; + } + + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + if (!value) { + return false; + } + + return NetUtils.isHTML(value); + }, + + renderXml(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!this.isXml(file)) { + return null; + } + + let doc = this.parseXml(file); + if (!doc) { + return null; + } + + // Proper component for rendering XML should be used (see bug 1247392) + return null; + }, + + /** + * Multipart post data are parsed and nicely rendered + * as an expandable tree of individual parts. + */ + renderMultiPart(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return; + } + + if (NetUtils.isMultiPartRequest(file)) { + // TODO: render multi part request (bug: 1247423) + } + + return; + }, + + /** + * URL encoded post data are nicely rendered as a list + * of parameters. + */ + renderUrlEncoded(file) { + let text = file.request.postData.text; + if (!text || isLongString(text)) { + return null; + } + + if (!NetUtils.isURLEncodedRequest(file)) { + return null; + } + + let lines = text.split("\n"); + let params = parseURLEncodedText(lines[lines.length - 1]); + + return { + key: "url-encoded", + content: NetInfoParams({params: params}), + name: Locale.$STR("netRequest.params") + }; + }, + + renderRawData(file) { + let text = file.request.postData.text; + + let group; + + // The post body might reached the limit, so check if we are + // dealing with a long string. + if (typeof text == "object") { + group = { + key: "raw-longstring", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + sanitize(text.initial), + SizeLimit({ + actions: this.props.actions, + data: file.request.postData, + message: Locale.$STR("netRequest.sizeLimitMessage"), + link: Locale.$STR("netRequest.sizeLimitMessageLink") + }) + ) + }; + } else { + group = { + key: "raw", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + sanitize(text) + ) + }; + } + + return group; + }, + + componentDidMount() { + let { actions, data: file } = this.props; + + if (!file.request.postData) { + // TODO: use async action objects as soon as Redux is in place + actions.requestData("requestPostData"); + } + }, + + render() { + let { actions, data: file } = this.props; + + if (file.discardRequestBody) { + return DOM.span({className: "netInfoBodiesDiscarded"}, + Locale.$STR("netRequest.requestBodyDiscarded") + ); + } + + if (!file.request.postData) { + return ( + Spinner() + ); + } + + // Render post body data. The right representation of the data + // is picked according to the content type. + let groups = []; + groups.push(this.renderUrlEncoded(file)); + // TODO: render multi part request (bug: 1247423) + // groups.push(this.renderMultiPart(file)); + groups.push(this.renderJson(file)); + groups.push(this.renderXml(file)); + groups.push(this.renderRawData(file)); + + // Filter out empty groups. + groups = groups.filter(group => group); + + // The raw response is collapsed by default if a nice formatted + // version is available. + if (groups.length > 1) { + groups[groups.length - 1].open = false; + } + + return ( + DOM.div({className: "postTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Helpers + +/** + * Workaround for a "not well-formed" error that react + * reports when there's multipart data passed to render. + */ +function sanitize(text) { + text = JSON.stringify(text); + text = text.replace(/\\r\\n/g, "\r\n").replace(/\\"/g, "\""); + return text.slice(1, text.length - 1); +} + +function isLongString(text) { + return typeof text == "object"; +} + +// Exports from this module +module.exports = PostTab; diff --git a/devtools/client/webconsole/net/components/response-tab.css b/devtools/client/webconsole/net/components/response-tab.css new file mode 100644 index 0000000000..e1c31fca43 --- /dev/null +++ b/devtools/client/webconsole/net/components/response-tab.css @@ -0,0 +1,21 @@ +/* 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/. */ + +/******************************************************************************/ +/* Response Tab */ + +.netInfoBody .netInfoBodiesDiscarded { + font-style: italic; + color: gray; +} + +.netInfoBody .netInfoResponseContent { + font-family: var(--monospace-font-family); + word-wrap: break-word; +} + +.netInfoBody .responseTabBox img { + max-width: 300px; + max-height: 300px; +} diff --git a/devtools/client/webconsole/net/components/response-tab.js b/devtools/client/webconsole/net/components/response-tab.js new file mode 100644 index 0000000000..78d8b2f778 --- /dev/null +++ b/devtools/client/webconsole/net/components/response-tab.js @@ -0,0 +1,277 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Reps +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view")); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); + +// Network +const SizeLimit = React.createFactory(require("./size-limit")); +const NetInfoGroupList = React.createFactory(require("./net-info-group-list")); +const Spinner = React.createFactory(require("./spinner")); +const Json = require("../utils/json"); +const NetUtils = require("../utils/net"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents 'Response' tab displayed when the user + * expands network log in the Console panel. It's responsible for + * rendering HTTP response body. + * + * In case of supported response mime-type (e.g. application/json, + * text/xml, etc.), the response is parsed using appropriate parser + * and rendered accordingly. + */ +var ResponseTab = React.createClass({ + propTypes: { + data: PropTypes.shape({ + request: PropTypes.object.isRequired, + response: PropTypes.object.isRequired + }), + actions: PropTypes.object.isRequired + }, + + displayName: "ResponseTab", + + // Response Types + + isJson(content) { + if (isLongString(content.text)) { + return false; + } + + return Json.isJSON(content.mimeType, content.text); + }, + + parseJson(file) { + let content = file.response.content; + if (isLongString(content.text)) { + return null; + } + + let jsonString = new String(content.text); + return Json.parseJSONString(jsonString); + }, + + isImage(content) { + if (isLongString(content.text)) { + return false; + } + + return NetUtils.isImage(content.mimeType); + }, + + isXml(content) { + if (isLongString(content.text)) { + return false; + } + + return NetUtils.isHTML(content.mimeType); + }, + + parseXml(file) { + let content = file.response.content; + if (isLongString(content.text)) { + return null; + } + + return NetUtils.parseXml(content); + }, + + // Rendering + + renderJson(file) { + let content = file.response.content; + if (!this.isJson(content)) { + return null; + } + + let json = this.parseJson(file); + if (!json) { + return null; + } + + return { + key: "json", + content: TreeView({ + columns: [{id: "value"}], + object: json, + mode: "tiny", + renderValue: props => Rep(Object.assign({}, props, { + cropLimit: 50, + })), + }), + name: Locale.$STR("jsonScopeName") + }; + }, + + renderImage(file) { + let content = file.response.content; + if (!this.isImage(content)) { + return null; + } + + let dataUri = "data:" + content.mimeType + ";base64," + content.text; + return { + key: "image", + content: DOM.img({src: dataUri}), + name: Locale.$STR("netRequest.image") + }; + }, + + renderXml(file) { + let content = file.response.content; + if (!this.isXml(content)) { + return null; + } + + let doc = this.parseXml(file); + if (!doc) { + return null; + } + + // Proper component for rendering XML should be used (see bug 1247392) + return null; + }, + + /** + * If full response text is available, let's try to parse and + * present nicely according to the underlying format. + */ + renderFormattedResponse(file) { + let content = file.response.content; + if (typeof content.text == "object") { + return null; + } + + let group = this.renderJson(file); + if (group) { + return group; + } + + group = this.renderImage(file); + if (group) { + return group; + } + + group = this.renderXml(file); + if (group) { + return group; + } + }, + + renderRawResponse(file) { + let group; + let content = file.response.content; + + // The response might reached the limit, so check if we are + // dealing with a long string. + if (typeof content.text == "object") { + group = { + key: "raw-longstring", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + content.text.initial, + SizeLimit({ + actions: this.props.actions, + data: content, + message: Locale.$STR("netRequest.sizeLimitMessage"), + link: Locale.$STR("netRequest.sizeLimitMessageLink") + }) + ) + }; + } else { + group = { + key: "raw", + name: Locale.$STR("netRequest.rawData"), + content: DOM.div({className: "netInfoResponseContent"}, + content.text + ) + }; + } + + return group; + }, + + componentDidMount() { + let { actions, data: file } = this.props; + let content = file.response.content; + + if (!content || typeof (content.text) == "undefined") { + // TODO: use async action objects as soon as Redux is in place + actions.requestData("responseContent"); + } + }, + + /** + * The response panel displays two groups: + * + * 1) Formatted response (in case of supported format, e.g. JSON, XML, etc.) + * 2) Raw response data (always displayed if not discarded) + */ + render() { + let { actions, data: file } = this.props; + + // If response bodies are discarded (not collected) let's just + // display a info message indicating what to do to collect even + // response bodies. + if (file.discardResponseBody) { + return DOM.span({className: "netInfoBodiesDiscarded"}, + Locale.$STR("netRequest.responseBodyDiscarded") + ); + } + + // Request for the response content is done only if the response + // is not fetched yet - i.e. the `content.text` is undefined. + // Empty content.text` can also be a valid response either + // empty or not available yet. + let content = file.response.content; + if (!content || typeof (content.text) == "undefined") { + return ( + Spinner() + ); + } + + // Render response body data. The right representation of the data + // is picked according to the content type. + let groups = []; + groups.push(this.renderFormattedResponse(file)); + groups.push(this.renderRawResponse(file)); + + // Filter out empty groups. + groups = groups.filter(group => group); + + // The raw response is collapsed by default if a nice formatted + // version is available. + if (groups.length > 1) { + groups[1].open = false; + } + + return ( + DOM.div({className: "responseTabBox"}, + DOM.div({className: "panelContent"}, + NetInfoGroupList({ + groups: groups + }) + ) + ) + ); + } +}); + +// Helpers + +function isLongString(text) { + return typeof text == "object"; +} + +// Exports from this module +module.exports = ResponseTab; diff --git a/devtools/client/webconsole/net/components/size-limit.css b/devtools/client/webconsole/net/components/size-limit.css new file mode 100644 index 0000000000..a5c214d9ea --- /dev/null +++ b/devtools/client/webconsole/net/components/size-limit.css @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/******************************************************************************/ +/* Response Size Limit */ + +.netInfoBody .netInfoSizeLimit { + font-weight: bold; + padding-top: 10px; +} + +.netInfoBody .netInfoSizeLimit .objectLink { + color: var(--theme-highlight-blue); +} diff --git a/devtools/client/webconsole/net/components/size-limit.js b/devtools/client/webconsole/net/components/size-limit.js new file mode 100644 index 0000000000..de88393148 --- /dev/null +++ b/devtools/client/webconsole/net/components/size-limit.js @@ -0,0 +1,62 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; +const PropTypes = React.PropTypes; + +/** + * This template represents a size limit notification message + * used e.g. in the Response tab when response body exceeds + * size limit. The message contains a link allowing the user + * to fetch the rest of the data from the backend (debugger server). + */ +var SizeLimit = React.createClass({ + propTypes: { + data: PropTypes.object.isRequired, + message: PropTypes.string.isRequired, + link: PropTypes.string.isRequired, + actions: PropTypes.shape({ + resolveString: PropTypes.func.isRequired + }), + }, + + displayName: "SizeLimit", + + // Event Handlers + + onClickLimit(event) { + let actions = this.props.actions; + let content = this.props.data; + + actions.resolveString(content, "text"); + }, + + // Rendering + + render() { + let message = this.props.message; + let link = this.props.link; + let reLink = /^(.*)\{\{link\}\}(.*$)/; + let m = message.match(reLink); + + return ( + DOM.div({className: "netInfoSizeLimit"}, + DOM.span({}, m[1]), + DOM.a({ + className: "objectLink", + onClick: this.onClickLimit}, + link + ), + DOM.span({}, m[2]) + ) + ); + } +}); + +// Exports from this module +module.exports = SizeLimit; diff --git a/devtools/client/webconsole/net/components/spinner.js b/devtools/client/webconsole/net/components/spinner.js new file mode 100644 index 0000000000..fe79f7dd19 --- /dev/null +++ b/devtools/client/webconsole/net/components/spinner.js @@ -0,0 +1,26 @@ +/* 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 React = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const DOM = React.DOM; + +/** + * This template represents a throbber displayed when the UI + * is waiting for data coming from the backend (debugging server). + */ +var Spinner = React.createClass({ + displayName: "Spinner", + + render() { + return ( + DOM.div({className: "devtools-throbber"}) + ); + } +}); + +// Exports from this module +module.exports = Spinner; diff --git a/devtools/client/webconsole/net/components/stacktrace-tab.js b/devtools/client/webconsole/net/components/stacktrace-tab.js new file mode 100644 index 0000000000..51eb7689ba --- /dev/null +++ b/devtools/client/webconsole/net/components/stacktrace-tab.js @@ -0,0 +1,29 @@ +/* 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 { PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react"); +const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace")); + +const StackTraceTab = createClass({ + displayName: "StackTraceTab", + + propTypes: { + data: PropTypes.object.isRequired, + actions: PropTypes.shape({ + onViewSourceInDebugger: PropTypes.func.isRequired + }) + }, + + render() { + let { stacktrace } = this.props.data.cause; + let { actions } = this.props; + let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions); + + return StackTrace({ stacktrace, onViewSourceInDebugger }); + } +}); + +// Exports from this module +module.exports = StackTraceTab; diff --git a/devtools/client/webconsole/net/data-provider.js b/devtools/client/webconsole/net/data-provider.js new file mode 100644 index 0000000000..d8a70d72d8 --- /dev/null +++ b/devtools/client/webconsole/net/data-provider.js @@ -0,0 +1,66 @@ +/* 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 promise = require("promise"); + +/** + * Map of pending requests. Used mainly by tests to wait + * till things are ready. + */ +var promises = new Map(); + +/** + * This object is used to fetch network data from the backend. + * Communication with the chrome scope is based on message + * exchange. + */ +var DataProvider = { + hasPendingRequests: function () { + return promises.size > 0; + }, + + requestData: function (client, actor, method) { + let key = actor + ":" + method; + let p = promises.get(key); + if (p) { + return p; + } + + let deferred = promise.defer(); + let realMethodName = "get" + method.charAt(0).toUpperCase() + + method.slice(1); + + if (!client[realMethodName]) { + return null; + } + + client[realMethodName](actor, response => { + promises.delete(key); + deferred.resolve(response); + }); + + promises.set(key, deferred.promise); + return deferred.promise; + }, + + resolveString: function (client, stringGrip) { + let key = stringGrip.actor + ":getString"; + let p = promises.get(key); + if (p) { + return p; + } + + p = client.getString(stringGrip).then(result => { + promises.delete(key); + return result; + }); + + promises.set(key, p); + return p; + }, +}; + +// Exports from this module +module.exports = DataProvider; diff --git a/devtools/client/webconsole/net/main.js b/devtools/client/webconsole/net/main.js new file mode 100644 index 0000000000..6fdf9494df --- /dev/null +++ b/devtools/client/webconsole/net/main.js @@ -0,0 +1,98 @@ +/* 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"; + +/* global BrowserLoader */ + +var { utils: Cu } = Components; + +// Initialize module loader and load all modules of the new inline +// preview feature. The entire code-base doesn't need any extra +// privileges and runs entirely in content scope. +const rootUrl = "resource://devtools/client/webconsole/net/"; +const require = BrowserLoader({ + baseURI: rootUrl, + window}).require; + +const NetRequest = require("./net-request"); +const { loadSheet } = require("sdk/stylesheet/utils"); + +// Localization +const {LocalizationHelper} = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper("devtools/client/locales/netmonitor.properties"); + +// Stylesheets +var styleSheets = [ + "resource://devtools/client/jsonview/css/toolbar.css", + "resource://devtools/client/shared/components/tree/tree-view.css", + "resource://devtools/client/shared/components/reps/reps.css", + "resource://devtools/client/webconsole/net/net-request.css", + "resource://devtools/client/webconsole/net/components/size-limit.css", + "resource://devtools/client/webconsole/net/components/net-info-body.css", + "resource://devtools/client/webconsole/net/components/net-info-group.css", + "resource://devtools/client/webconsole/net/components/net-info-params.css", + "resource://devtools/client/webconsole/net/components/response-tab.css" +]; + +// Load theme stylesheets into the Console frame. This should be +// done automatically by UI Components as soon as we have consensus +// on the right CSS strategy FIXME. +// It would also be nice to include them using @import. +styleSheets.forEach(url => { + loadSheet(this, url, "author"); +}); + +// Localization API used by React components +// accessing strings from *.properties file. +// Example: +// let localizedString = Locale.$STR('string-key'); +// +// Resources: +// http://l20n.org/ +// https://github.com/yahoo/react-intl +this.Locale = { + $STR: key => { + try { + return L10N.getStr(key); + } catch (err) { + console.error(key + ": " + err); + } + } +}; + +// List of NetRequest instances represents the state. +// As soon as Redux is in place it should be maintained using a reducer. +var netRequests = new Map(); + +/** + * This function handles network events received from the backend. It's + * executed from within the webconsole.js + */ +function onNetworkEvent(log) { + // The 'from' field is set only in case of a 'networkEventUpdate' packet. + // The initial 'networkEvent' packet uses 'actor'. + // Check if NetRequest object is already created for this event actor and + // if there is none make sure to create one. + let response = log.response; + let netRequest = response.from ? netRequests.get(response.from) : null; + if (!netRequest && !log.update) { + netRequest = new NetRequest(log); + netRequests.set(response.actor, netRequest); + } + + if (!netRequest) { + return; + } + + if (log.update) { + netRequest.updateBody(response); + } + + return; +} + +// Make the 'onNetworkEvent' accessible from chrome (see webconsole.js) +this.NetRequest = { + onNetworkEvent: onNetworkEvent +}; diff --git a/devtools/client/webconsole/net/moz.build b/devtools/client/webconsole/net/moz.build new file mode 100644 index 0000000000..1b9eca7fe2 --- /dev/null +++ b/devtools/client/webconsole/net/moz.build @@ -0,0 +1,19 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'components', + 'utils' +] + +DevToolsModules( + 'data-provider.js', + 'main.js', + 'net-request.css', + 'net-request.js', +) + +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] +BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini'] diff --git a/devtools/client/webconsole/net/net-request.css b/devtools/client/webconsole/net/net-request.css new file mode 100644 index 0000000000..82b6a027f0 --- /dev/null +++ b/devtools/client/webconsole/net/net-request.css @@ -0,0 +1,35 @@ + /* 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/. */ + +/******************************************************************************/ +/* General */ + +:root { + --net-border: #d7d7d7; +} + +:root.theme-dark { + --net-border: #5f7387; +} + +/******************************************************************************/ +/* Network log */ + +/* No background if a Net log is opened */ +.netRequest.message.opened, +.netRequest.message.opened:hover { + background: transparent !important; +} + +/******************************************************************************/ +/* Themes */ + +.theme-dark .netRequest.opened:hover, +.theme-dark .netRequest.opened { + background: transparent; +} + +.theme-firebug .netRequest.message.opened:hover { + background-image: linear-gradient(rgba(214, 233, 246, 0.8), rgba(255, 255, 255, 1.6)) !important; +} diff --git a/devtools/client/webconsole/net/net-request.js b/devtools/client/webconsole/net/net-request.js new file mode 100644 index 0000000000..48cf66fddf --- /dev/null +++ b/devtools/client/webconsole/net/net-request.js @@ -0,0 +1,323 @@ +/* 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"; + +// React +const React = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); + +// Reps +const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils"); + +// Network +const { cancelEvent, isLeftClick } = require("./utils/events"); +const NetInfoBody = React.createFactory(require("./components/net-info-body")); +const DataProvider = require("./data-provider"); + +// Constants +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * This object represents a network log in the Console panel (and in the + * Network panel in the future). + * It's associated with an existing log and so, also with an existing + * element in the DOM. + * + * The object neither render no request for more data by default. It only + * reqisters a click listener to the associated log entry (a network event) + * and changes the class attribute of the log entry, so a twisty icon + * appears to indicates that there are more details displayed if the + * log entry is expanded. + * + * When the user expands the log, data are requested from the backend + * and rendered directly within the Console iframe. + */ +function NetRequest(log) { + this.initialize(log); +} + +NetRequest.prototype = { + initialize: function (log) { + this.client = log.consoleFrame.webConsoleClient; + this.owner = log.consoleFrame.owner; + + // 'this.file' field is following HAR spec. + // http://www.softwareishard.com/blog/har-12-spec/ + this.file = log.response; + this.parentNode = log.node; + this.file.request.queryString = parseURLParams(this.file.request.url); + this.hasCookies = false; + + // Map of fetched responses (to avoid unnecessary RDP round trip). + this.cachedResponses = new Map(); + + let doc = this.parentNode.ownerDocument; + let twisty = doc.createElementNS(XHTML_NS, "a"); + twisty.className = "theme-twisty"; + twisty.href = "#"; + + let messageBody = this.parentNode.querySelector(".message-body-wrapper"); + this.parentNode.insertBefore(twisty, messageBody); + this.parentNode.setAttribute("collapsible", true); + + this.parentNode.classList.add("netRequest"); + + // Register a click listener. + this.addClickListener(); + }, + + addClickListener: function () { + // Add an event listener to toggle the expanded state when clicked. + // The event bubbling is canceled if the user clicks on the log + // itself (not on the expanded body), so opening of the default + // modal dialog is avoided. + this.parentNode.addEventListener("click", (event) => { + if (!isLeftClick(event)) { + return; + } + + // Clicking on the toggle button or the method expands/collapses + // the body with HTTP details. + let classList = event.originalTarget.classList; + if (!(classList.contains("theme-twisty") || + classList.contains("method"))) { + return; + } + + // Alright, the user is clicking fine, let's open HTTP details! + this.onToggleBody(event); + + // Avoid the default modal dialog + cancelEvent(event); + }, true); + }, + + onToggleBody: function (event) { + let target = event.currentTarget; + let logRow = target.closest(".netRequest"); + logRow.classList.toggle("opened"); + + let twisty = this.parentNode.querySelector(".theme-twisty"); + if (logRow.classList.contains("opened")) { + twisty.setAttribute("open", true); + } else { + twisty.removeAttribute("open"); + } + + let isOpen = logRow.classList.contains("opened"); + if (isOpen) { + this.renderBody(); + } else { + this.closeBody(); + } + }, + + updateCookies: function(method, response) { + // TODO: This code will be part of a reducer. + let result; + if (response.cookies > 0 && + ["requestCookies", "responseCookies"].includes(method)) { + this.hasCookies = true; + this.refresh(); + } + }, + + /** + * Executed when 'networkEventUpdate' is received from the backend. + */ + updateBody: function (response) { + // 'networkEventUpdate' event indicates that there are new data + // available on the backend. The following logic checks the response + // cache and if this data has been already requested before they + // need to be updated now (re-requested). + let method = response.updateType; + this.updateCookies(method, response); + if (this.cachedResponses.get(method)) { + this.cachedResponses.delete(method); + this.requestData(method); + } + }, + + /** + * Close network inline preview body. + */ + closeBody: function () { + this.netInfoBodyBox.parentNode.removeChild(this.netInfoBodyBox); + }, + + /** + * Render network inline preview body. + */ + renderBody: function () { + let messageBody = this.parentNode.querySelector(".message-body-wrapper"); + + // Create box for all markup rendered by ReactJS. Since we are + // rendering within webconsole.xul (i.e. XUL document) we need + // to explicitly specify XHTML namespace. + let doc = messageBody.ownerDocument; + this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div"); + this.netInfoBodyBox.classList.add("netInfoBody"); + messageBody.appendChild(this.netInfoBodyBox); + + // As soon as Redux is in place state and actions will come from + // separate modules. + let body = NetInfoBody({ + actions: this + }); + + // Render net info body! + this.body = ReactDOM.render(body, this.netInfoBodyBox); + + this.refresh(); + }, + + /** + * Render top level ReactJS component. + */ + refresh: function () { + if (!this.netInfoBodyBox) { + return; + } + + // TODO: As soon as Redux is in place there will be reducer + // computing a new state. + let newState = Object.assign({}, this.body.state, { + data: this.file, + hasCookies: this.hasCookies + }); + + this.body.setState(newState); + }, + + // Communication with the backend + + requestData: function (method) { + // If the response has already been received bail out. + let response = this.cachedResponses.get(method); + if (response) { + return; + } + + // Set an attribute indicating that this net log is waiting for + // data coming from the backend. Intended mainly for tests. + this.parentNode.setAttribute("loading", "true"); + + let actor = this.file.actor; + DataProvider.requestData(this.client, actor, method).then(args => { + this.cachedResponses.set(method, args); + this.onRequestData(method, args); + + if (!DataProvider.hasPendingRequests()) { + this.parentNode.removeAttribute("loading"); + + // Fire an event indicating that all pending requests for + // data from the backend has finished. Intended for tests. + // Do it asynchronously so, it's done after all handlers + // for the current promise are executed. + setTimeout(() => { + let event = document.createEvent("Event"); + event.initEvent("netlog-no-pending-requests", true, true); + this.parentNode.dispatchEvent(event); + }); + } + }); + }, + + onRequestData: function (method, response) { + // TODO: This code will be part of a reducer. + let result; + switch (method) { + case "requestHeaders": + result = this.onRequestHeaders(response); + break; + case "responseHeaders": + result = this.onResponseHeaders(response); + break; + case "requestCookies": + result = this.onRequestCookies(response); + break; + case "responseCookies": + result = this.onResponseCookies(response); + break; + case "responseContent": + result = this.onResponseContent(response); + break; + case "requestPostData": + result = this.onRequestPostData(response); + break; + } + + result.then(() => { + this.refresh(); + }); + }, + + onRequestHeaders: function (response) { + this.file.request.headers = response.headers; + + return this.resolveHeaders(this.file.request.headers); + }, + + onResponseHeaders: function (response) { + this.file.response.headers = response.headers; + + return this.resolveHeaders(this.file.response.headers); + }, + + onResponseContent: function (response) { + let content = response.content; + + for (let p in content) { + this.file.response.content[p] = content[p]; + } + + return Promise.resolve(); + }, + + onRequestPostData: function (response) { + this.file.request.postData = response.postData; + return Promise.resolve(); + }, + + onRequestCookies: function (response) { + this.file.request.cookies = response.cookies; + return this.resolveHeaders(this.file.request.cookies); + }, + + onResponseCookies: function (response) { + this.file.response.cookies = response.cookies; + return this.resolveHeaders(this.file.response.cookies); + }, + + onViewSourceInDebugger: function (frame) { + this.owner.viewSourceInDebugger(frame.source, frame.line); + }, + + resolveHeaders: function (headers) { + let promises = []; + + for (let header of headers) { + if (typeof header.value == "object") { + promises.push(this.resolveString(header.value).then(value => { + header.value = value; + })); + } + } + + return Promise.all(promises); + }, + + resolveString: function (object, propName) { + let stringGrip = object[propName]; + if (typeof stringGrip == "object") { + DataProvider.resolveString(this.client, stringGrip).then(args => { + object[propName] = args; + this.refresh(); + }); + } + } +}; + +// Exports from this module +module.exports = NetRequest; diff --git a/devtools/client/webconsole/net/test/mochitest/.eslintrc.js b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..76904829d5 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../../.eslintrc.mochitests.js", +}; diff --git a/devtools/client/webconsole/net/test/mochitest/browser.ini b/devtools/client/webconsole/net/test/mochitest/browser.ini new file mode 100644 index 0000000000..9414414c6d --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + page_basic.html + test.json + test.json^headers^ + test-cookies.json + test-cookies.json^headers^ + test.txt + test.xml + test.xml^headers^ + !/devtools/client/webconsole/test/head.js + !/devtools/client/framework/test/shared-head.js + +[browser_net_basic.js] +[browser_net_cookies.js] +[browser_net_headers.js] +[browser_net_params.js] +[browser_net_post.js] +[browser_net_response.js] diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js new file mode 100644 index 0000000000..57273bec06 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js @@ -0,0 +1,33 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const JSON_XHR_URL = URL_ROOT + "test.json"; + +/** + * Basic test that generates XHR in the content and + * checks the related log in the Console panel can + * be expanded. + */ +add_task(function* () { + info("Test XHR Spy basic started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL + }); + + ok(netInfoBody, "The network details must be available"); + + // There should be at least two tabs: Headers and Response + ok(netInfoBody.querySelector(".tabs .tabs-menu-item.headers"), + "Headers tab must be available"); + ok(netInfoBody.querySelector(".tabs .tabs-menu-item.response"), + "Response tab must be available"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js new file mode 100644 index 0000000000..cfd85c2edb --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const JSON_XHR_URL = URL_ROOT + "test-cookies.json"; + +/** + * This test generates XHR requests in the page, expands + * networks details in the Console panel and checks that + * Cookies are properly displayed. + */ +add_task(function* () { + info("Test XHR Spy cookies started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL + }); + + // Select "Cookies" tab + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "cookies"); + + let requestCookieName = tabBody.querySelector( + ".netInfoGroup.requestCookies .netInfoParamName > span[title='bar']"); + + // Verify request cookies (name and value) + ok(requestCookieName, "Request Cookie name must exist"); + is(requestCookieName.textContent, "bar", + "The cookie name must have proper value"); + + let requestCookieValue = requestCookieName.parentNode.nextSibling; + ok(requestCookieValue, "Request Cookie value must exist"); + is(requestCookieValue.textContent, "foo", + "The cookie value must have proper value"); + + let responseCookieName = tabBody.querySelector( + ".netInfoGroup.responseCookies .netInfoParamName > span[title='test']"); + + // Verify response cookies (name and value) + ok(responseCookieName, "Response Cookie name must exist"); + is(responseCookieName.textContent, "test", + "The cookie name must have proper value"); + + let responseCookieValue = responseCookieName.parentNode.nextSibling; + ok(responseCookieValue, "Response Cookie value must exist"); + is(responseCookieValue.textContent, "abc", + "The cookie value must have proper value"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js new file mode 100644 index 0000000000..4a47074ee1 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const JSON_XHR_URL = URL_ROOT + "test.json"; + +/** + * This test generates XHR requests in the page, expands + * networks details in the Console panel and checks that + * HTTP headers are there. + */ +add_task(function* () { + info("Test XHR Spy headers started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL + }); + + // Select "Headers" tab + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "headers"); + let paramName = tabBody.querySelector( + ".netInfoParamName > span[title='Content-Type']"); + + // Verify "Content-Type" header (name and value) + ok(paramName, "Header name must exist"); + is(paramName.textContent, "Content-Type", + "The header name must have proper value"); + + let paramValue = paramName.parentNode.nextSibling; + ok(paramValue, "Header value must exist"); + is(paramValue.textContent, "application/json; charset=utf-8", + "The header value must have proper value"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_params.js b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js new file mode 100644 index 0000000000..d8b0e2c84b --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const JSON_XHR_URL = URL_ROOT + "test.json"; + +/** + * This test generates XHR requests in the page, expands + * networks details in the Console panel and checks that + * HTTP parameters (query string) are there. + */ +add_task(function* () { + info("Test XHR Spy params started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL, + queryString: "?foo=bar" + }); + + // Check headers + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params"); + + let paramName = tabBody.querySelector( + ".netInfoParamName > span[title='foo']"); + + // Verify "Content-Type" header (name and value) + ok(paramName, "Header name must exist"); + is(paramName.textContent, "foo", + "The param name must have proper value"); + + let paramValue = paramName.parentNode.nextSibling; + ok(paramValue, "param value must exist"); + is(paramValue.textContent, "bar", + "The param value must have proper value"); +}); + +/** + * Test URL parameters with the same name. + */ +add_task(function* () { + info("Test XHR Spy params started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL, + queryString: "?box[]=123&box[]=456" + }); + + // Check headers + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params"); + + let params = tabBody.querySelectorAll( + ".netInfoParamName > span[title='box[]']"); + is(params.length, 2, "Two URI parameters must exist"); + + let values = tabBody.querySelectorAll( + ".netInfoParamValue > code"); + is(values[0].textContent, 123, "First value must match"); + is(values[1].textContent, 456, "Second value must match"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_post.js b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js new file mode 100644 index 0000000000..f6e776ef0f --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js @@ -0,0 +1,88 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const JSON_XHR_URL = URL_ROOT + "test.json"; + +const plainPostBody = "test-data"; +const jsonData = "{\"bar\": \"baz\"}"; +const jsonRendered = "bar\"baz\""; +const xmlPostBody = "<xml><name>John</name></xml>"; + +/** + * This test generates XHR requests in the page, expands + * networks details in the Console panel and checks that + * Post data are properly rendered. + */ +add_task(function* () { + info("Test XHR Spy post plain body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "POST", + url: JSON_XHR_URL, + body: plainPostBody + }); + + // Check post body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post"); + let postContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + is(postContent.textContent, plainPostBody, + "Post body must be properly rendered"); +}); + +add_task(function* () { + info("Test XHR Spy post JSON body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "POST", + url: JSON_XHR_URL, + body: jsonData, + requestHeaders: [{ + name: "Content-Type", + value: "application/json" + }] + }); + + // Check post body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post"); + let postContent = tabBody.querySelector( + ".netInfoGroup.json.opened .netInfoGroupContent"); + is(postContent.textContent, jsonRendered, + "Post body must be properly rendered"); + + let rawPostContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + ok(!rawPostContent, "Raw response group must be collapsed"); +}); + +add_task(function* () { + info("Test XHR Spy post XML body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "POST", + url: JSON_XHR_URL, + body: xmlPostBody, + requestHeaders: [{ + name: "Content-Type", + value: "application/xml" + }] + }); + + // Check post body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post"); + let rawPostContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + is(rawPostContent.textContent, xmlPostBody, + "Raw response group must not be collapsed"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_response.js b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js new file mode 100644 index 0000000000..ec5543043e --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js @@ -0,0 +1,86 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_PAGE_URL = URL_ROOT + "page_basic.html"; +const TEXT_XHR_URL = URL_ROOT + "test.txt"; +const JSON_XHR_URL = URL_ROOT + "test.json"; +const XML_XHR_URL = URL_ROOT + "test.xml"; + +const textResponseBody = "this is a response"; +const jsonResponseBody = "name\"John\""; + +// Individual tests below generate XHR request in the page, expand +// network details in the Console panel and checks various types +// of response bodies. + +/** + * Validate plain text response + */ +add_task(function* () { + info("Test XHR Spy respone plain body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: TEXT_XHR_URL, + }); + + // Check response body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response"); + let responseContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + + ok(responseContent.textContent.indexOf(textResponseBody) > -1, + "Response body must be properly rendered"); +}); + +/** + * Validate XML response + */ +add_task(function* () { + info("Test XHR Spy response XML body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: XML_XHR_URL, + }); + + // Check response body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response"); + let rawResponseContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + ok(rawResponseContent, "Raw response group must not be collapsed"); +}); + +/** + * Validate JSON response + */ +add_task(function* () { + info("Test XHR Spy response JSON body started"); + + let {hud} = yield addTestTab(TEST_PAGE_URL); + + let netInfoBody = yield executeAndInspectXhr(hud, { + method: "GET", + url: JSON_XHR_URL, + }); + + // Check response body data + let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response"); + let responseContent = tabBody.querySelector( + ".netInfoGroup.json .netInfoGroupContent"); + + is(responseContent.textContent, jsonResponseBody, + "Response body must be properly rendered"); + + let rawResponseContent = tabBody.querySelector( + ".netInfoGroup.raw.opened .netInfoGroupContent"); + ok(!rawResponseContent, "Raw response group must be collapsed"); +}); diff --git a/devtools/client/webconsole/net/test/mochitest/head.js b/devtools/client/webconsole/net/test/mochitest/head.js new file mode 100644 index 0000000000..c012069482 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/head.js @@ -0,0 +1,209 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ +/* import-globals-from ../../../test/head.js */ + +"use strict"; + +// Load Web Console head.js, it implements helper console test API +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/webconsole/test/head.js", this); + +const FRAME_SCRIPT_UTILS_URL = + "chrome://devtools/content/shared/frame-script-utils.js"; + +const NET_INFO_PREF = "devtools.webconsole.filter.networkinfo"; +const NET_XHR_PREF = "devtools.webconsole.filter.netxhr"; + +// Enable XHR logging for the test +Services.prefs.setBoolPref(NET_INFO_PREF, true); +Services.prefs.setBoolPref(NET_XHR_PREF, true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(NET_INFO_PREF, true); + Services.prefs.clearUserPref(NET_XHR_PREF, true); +}); + +// Use the old webconsole since the new one doesn't yet support +// XHR spy. See Bug 1304794. +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); +}); + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url The url to be loaded in the new tab + * @return a promise that resolves to the tab object when the url is loaded + */ +function addTestTab(url) { + info("Adding a new JSON tab with URL: '" + url + "'"); + + return Task.spawn(function* () { + let tab = yield addTab(url); + + // Load devtools/shared/frame-script-utils.js + loadCommonFrameScript(tab); + + // Open the Console panel + let hud = yield openConsole(); + + return { + tab: tab, + browser: tab.linkedBrowser, + hud: hud + }; + }); +} + +/** + * + * @param hud + * @param options + */ +function executeAndInspectXhr(hud, options) { + hud.jsterm.clearOutput(); + + options.queryString = options.queryString || ""; + + // Execute XHR in the content scope. + performRequestsInContent({ + method: options.method, + url: options.url + options.queryString, + body: options.body, + nocache: options.nocache, + requestHeaders: options.requestHeaders + }); + + return Task.spawn(function* () { + // Wait till the appropriate Net log appears in the Console panel. + let rules = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: options.url, + category: CATEGORY_NETWORK, + severity: SEVERITY_INFO, + isXhr: true, + }] + }); + + // The log is here, get its parent element (className: 'message'). + let msg = [...rules[0].matched][0]; + let body = msg.querySelector(".message-body"); + + // Open XHR HTTP details body and wait till the UI fetches + // all necessary data from the backend. All RPD requests + // needs to be finished before we can continue testing. + yield synthesizeMouseClickSoon(hud, body); + yield waitForBackend(msg); + let netInfoBody = body.querySelector(".netInfoBody"); + ok(netInfoBody, "Net info body must exist"); + return netInfoBody; + }); +} + +/** + * Wait till XHR data are fetched from the backend (i.e. there are + * no pending RDP requests. + */ +function waitForBackend(element) { + if (!element.hasAttribute("loading")) { + return; + } + return once(element, "netlog-no-pending-requests", true); +} + +/** + * Select specific tab in XHR info body. + * + * @param netInfoBody The main XHR info body + * @param tabId Tab ID (possible values: 'headers', 'cookies', 'params', + * 'post', 'response'); + * + * @returns Tab body element. + */ +function selectNetInfoTab(hud, netInfoBody, tabId) { + let tab = netInfoBody.querySelector(".tabs-menu-item." + tabId); + ok(tab, "Tab must exist " + tabId); + + // Click to select specified tab and wait till its + // UI is populated with data from the backend. + // There must be no pending RDP requests before we can + // continue testing the UI. + return Task.spawn(function* () { + yield synthesizeMouseClickSoon(hud, tab); + let msg = getAncestorByClass(netInfoBody, "message"); + yield waitForBackend(msg); + let tabBody = netInfoBody.querySelector("." + tabId + "TabBox"); + ok(tabBody, "Tab body must exist"); + return tabBody; + }); +} + +/** + * Return parent node with specified class. + * + * @param node A child element + * @param className Specified class name. + * + * @returns A parent element. + */ +function getAncestorByClass(node, className) { + for (let parent = node; parent; parent = parent.parentNode) { + if (parent.classList && parent.classList.contains(className)) { + return parent; + } + } + return null; +} + +/** + * Synthesize asynchronous click event (with clean stack trace). + */ +function synthesizeMouseClickSoon(hud, element) { + return new Promise((resolve) => { + executeSoon(() => { + EventUtils.synthesizeMouse(element, 2, 2, {}, hud.iframeWindow); + resolve(); + }); + }); +} + +/** + * Execute XHR in the content scope. + */ +function performRequestsInContent(requests) { + info("Performing requests in the context of the content."); + return executeInContent("devtools:test:xhr", requests); +} + +function executeInContent(name, data = {}, objects = {}, + expectResponse = true) { + let mm = gBrowser.selectedBrowser.messageManager; + + mm.sendAsyncMessage(name, data, objects); + if (expectResponse) { + return waitForContentMessage(name); + } + + return Promise.resolve(); +} + +function waitForContentMessage(name) { + info("Expecting message " + name + " from content"); + + let mm = gBrowser.selectedBrowser.messageManager; + + return new Promise((resolve) => { + mm.addMessageListener(name, function onMessage(msg) { + mm.removeMessageListener(name, onMessage); + resolve(msg.data); + }); + }); +} + +function loadCommonFrameScript(tab) { + let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser; + browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false); +} diff --git a/devtools/client/webconsole/net/test/mochitest/page_basic.html b/devtools/client/webconsole/net/test/mochitest/page_basic.html new file mode 100644 index 0000000000..da71584926 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/page_basic.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>XHR Spy test page</title> + </head> + <body> + <script type="text/javascript"> + document.cookie = "bar=foo"; + </script> + </body> +</html> diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json b/devtools/client/webconsole/net/test/mochitest/test-cookies.json new file mode 100644 index 0000000000..b5e739025b --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json @@ -0,0 +1 @@ +{"name":"Cookies Test"} diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ new file mode 100644 index 0000000000..94a8c0c69d --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json; charset=utf-8 +Set-Cookie: test=abc diff --git a/devtools/client/webconsole/net/test/mochitest/test.json b/devtools/client/webconsole/net/test/mochitest/test.json new file mode 100644 index 0000000000..6548f8e3e9 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test.json @@ -0,0 +1 @@ +{"name":"John"} diff --git a/devtools/client/webconsole/net/test/mochitest/test.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 diff --git a/devtools/client/webconsole/net/test/mochitest/test.txt b/devtools/client/webconsole/net/test/mochitest/test.txt new file mode 100644 index 0000000000..af7014e116 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test.txt @@ -0,0 +1 @@ +this is a response diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml b/devtools/client/webconsole/net/test/mochitest/test.xml new file mode 100644 index 0000000000..3749c8e5a7 --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test.xml @@ -0,0 +1 @@ +<xml><name>John</name></xml> diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ new file mode 100644 index 0000000000..10ecdf5f4d --- /dev/null +++ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ @@ -0,0 +1 @@ +Content-Type: application/xml; charset=utf-8 diff --git a/devtools/client/webconsole/net/test/unit/.eslintrc.js b/devtools/client/webconsole/net/test/unit/.eslintrc.js new file mode 100644 index 0000000000..54a9a6361b --- /dev/null +++ b/devtools/client/webconsole/net/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/client/webconsole/net/test/unit/test_json-utils.js b/devtools/client/webconsole/net/test/unit/test_json-utils.js new file mode 100644 index 0000000000..f8ccdf3aa8 --- /dev/null +++ b/devtools/client/webconsole/net/test/unit/test_json-utils.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +var Cu = Components.utils; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { parseJSONString, isJSON } = require("devtools/client/webconsole/net/utils/json"); + +// Test data +const simpleJson = '{"name":"John"}'; +const jsonInFunc = 'someFunc({"name":"John"})'; + +const json1 = "{'a': 1}"; +const json2 = " {'a': 1}"; +const json3 = "\t {'a': 1}"; +const json4 = "\n\n\t {'a': 1}"; +const json5 = "\n\n\t "; + +const textMimeType = "text/plain"; +const jsonMimeType = "text/javascript"; +const unknownMimeType = "text/unknown"; + +/** + * Testing API provided by webconsole/net/utils/json.js + */ +function run_test() { + // parseJSONString + equal(parseJSONString(simpleJson).name, "John"); + equal(parseJSONString(jsonInFunc).name, "John"); + + // isJSON + equal(isJSON(textMimeType, json1), true); + equal(isJSON(textMimeType, json2), true); + equal(isJSON(jsonMimeType, json3), true); + equal(isJSON(jsonMimeType, json4), true); + + equal(isJSON(unknownMimeType, json1), true); + equal(isJSON(textMimeType, json1), true); + + equal(isJSON(unknownMimeType), false); + equal(isJSON(unknownMimeType, json5), false); +} diff --git a/devtools/client/webconsole/net/test/unit/test_net-utils.js b/devtools/client/webconsole/net/test/unit/test_net-utils.js new file mode 100644 index 0000000000..512ebcbc7f --- /dev/null +++ b/devtools/client/webconsole/net/test/unit/test_net-utils.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +var Cu = Components.utils; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { + isImage, + isHTML, + getHeaderValue, + isURLEncodedRequest, + isMultiPartRequest +} = require("devtools/client/webconsole/net/utils/net"); + +// Test data +const imageMimeTypes = ["image/jpeg", "image/jpg", "image/gif", + "image/png", "image/bmp"]; + +const htmlMimeTypes = ["text/html", "text/xml", "application/xml", + "application/rss+xml", "application/atom+xml", "application/xhtml+xml", + "application/mathml+xml", "application/rdf+xml"]; + +const headers = [{name: "headerName", value: "value1"}]; + +const har1 = { + request: { + postData: { + text: "content-type: application/x-www-form-urlencoded" + } + } +}; + +const har2 = { + request: { + headers: [{ + name: "content-type", + value: "application/x-www-form-urlencoded" + }] + } +}; + +const har3 = { + request: { + headers: [{ + name: "content-type", + value: "multipart/form-data" + }] + } +}; + +/** + * Testing API provided by webconsole/net/utils/net.js + */ +function run_test() { + // isImage + imageMimeTypes.forEach(mimeType => { + ok(isImage(mimeType)); + }); + + // isHTML + htmlMimeTypes.forEach(mimeType => { + ok(isHTML(mimeType)); + }); + + // getHeaderValue + equal(getHeaderValue(headers, "headerName"), "value1"); + + // isURLEncodedRequest + ok(isURLEncodedRequest(har1)); + ok(isURLEncodedRequest(har2)); + + // isMultiPartRequest + ok(isMultiPartRequest(har3)); +} diff --git a/devtools/client/webconsole/net/test/unit/xpcshell.ini b/devtools/client/webconsole/net/test/unit/xpcshell.ini new file mode 100644 index 0000000000..d988a2ad02 --- /dev/null +++ b/devtools/client/webconsole/net/test/unit/xpcshell.ini @@ -0,0 +1,9 @@ +[DEFAULT] +tags = devtools +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_json-utils.js] +[test_net-utils.js] diff --git a/devtools/client/webconsole/net/utils/events.js b/devtools/client/webconsole/net/utils/events.js new file mode 100644 index 0000000000..9f8705593a --- /dev/null +++ b/devtools/client/webconsole/net/utils/events.js @@ -0,0 +1,21 @@ +/* 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 isLeftClick(event, allowKeyModifiers) { + return event.button === 0 && (allowKeyModifiers || noKeyModifiers(event)); +} + +function noKeyModifiers(event) { + return !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey; +} + +function cancelEvent(event) { + event.stopPropagation(); + event.preventDefault(); +} + +// Exports from this module +exports.isLeftClick = isLeftClick; +exports.cancelEvent = cancelEvent; diff --git a/devtools/client/webconsole/net/utils/json.js b/devtools/client/webconsole/net/utils/json.js new file mode 100644 index 0000000000..70d733f281 --- /dev/null +++ b/devtools/client/webconsole/net/utils/json.js @@ -0,0 +1,234 @@ +/* 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"; + +// List of JSON content types. +const contentTypes = { + "text/plain": 1, + "text/javascript": 1, + "text/x-javascript": 1, + "text/json": 1, + "text/x-json": 1, + "application/json": 1, + "application/x-json": 1, + "application/javascript": 1, + "application/x-javascript": 1, + "application/json-rpc": 1 +}; + +// Implementation +var Json = {}; + +/** + * Parsing JSON + */ +Json.parseJSONString = function (jsonString) { + if (!jsonString.length) { + return null; + } + + let regex, matches; + + let first = firstNonWs(jsonString); + if (first !== "[" && first !== "{") { + // This (probably) isn't pure JSON. Let's try to strip various sorts + // of XSSI protection/wrapping and see if that works better. + + // Prototype-style secure requests + regex = /^\s*\/\*-secure-([\s\S]*)\*\/\s*$/; + matches = regex.exec(jsonString); + if (matches) { + jsonString = matches[1]; + + if (jsonString[0] === "\\" && jsonString[1] === "n") { + jsonString = jsonString.substr(2); + } + + if (jsonString[jsonString.length - 2] === "\\" && + jsonString[jsonString.length - 1] === "n") { + jsonString = jsonString.substr(0, jsonString.length - 2); + } + } + + // Google-style (?) delimiters + if (jsonString.indexOf("&&&START&&&") !== -1) { + regex = /&&&START&&&([\s\S]*)&&&END&&&/; + matches = regex.exec(jsonString); + if (matches) { + jsonString = matches[1]; + } + } + + // while(1);, for(;;);, and )]}' + regex = /^\s*(\)\]\}[^\n]*\n|while\s*\(1\);|for\s*\(;;\);)([\s\S]*)/; + matches = regex.exec(jsonString); + if (matches) { + jsonString = matches[2]; + } + + // JSONP + regex = /^\s*([A-Za-z0-9_$.]+\s*(?:\[.*\]|))\s*\(([\s\S]*)\)/; + matches = regex.exec(jsonString); + if (matches) { + jsonString = matches[2]; + } + } + + try { + return JSON.parse(jsonString); + } catch (err) { + // eslint-disable-line no-empty + } + + // Give up if we don't have valid start, to avoid some unnecessary overhead. + first = firstNonWs(jsonString); + if (first !== "[" && first !== "{" && isNaN(first) && first !== '"') { + return null; + } + + // Remove JavaScript comments, quote non-quoted identifiers, and merge + // multi-line structures like |{"a": 1} \n {"b": 2}| into a single JSON + // object [{"a": 1}, {"b": 2}]. + jsonString = pseudoJsonToJson(jsonString); + + try { + return JSON.parse(jsonString); + } catch (err) { + // eslint-disable-line no-empty + } + + return null; +}; + +function firstNonWs(str) { + for (let i = 0, len = str.length; i < len; i++) { + let ch = str[i]; + if (ch !== " " && ch !== "\n" && ch !== "\t" && ch !== "\r") { + return ch; + } + } + return ""; +} + +function pseudoJsonToJson(json) { + let ret = ""; + let at = 0, lasti = 0, lastch = "", hasMultipleParts = false; + for (let i = 0, len = json.length; i < len; ++i) { + let ch = json[i]; + if (/\s/.test(ch)) { + continue; + } + + if (ch === '"') { + // Consume a string. + ++i; + while (i < len) { + if (json[i] === "\\") { + ++i; + } else if (json[i] === '"') { + break; + } + ++i; + } + } else if (ch === "'") { + // Convert an invalid string into a valid one. + ret += json.slice(at, i) + "\""; + at = i + 1; + ++i; + + while (i < len) { + if (json[i] === "\\") { + ++i; + } else if (json[i] === "'") { + break; + } + ++i; + } + + if (i < len) { + ret += json.slice(at, i) + "\""; + at = i + 1; + } + } else if ((ch === "[" || ch === "{") && + (lastch === "]" || lastch === "}")) { + // Multiple JSON messages in one... Make it into a single array by + // inserting a comma and setting the "multiple parts" flag. + ret += json.slice(at, i) + ","; + hasMultipleParts = true; + at = i; + } else if (lastch === "," && (ch === "]" || ch === "}")) { + // Trailing commas in arrays/objects. + ret += json.slice(at, lasti); + at = i; + } else if (lastch === "/" && lasti === i - 1) { + // Some kind of comment; remove it. + if (ch === "/") { + ret += json.slice(at, i - 1); + at = i + json.slice(i).search(/\n|\r|$/); + i = at - 1; + } else if (ch === "*") { + ret += json.slice(at, i - 1); + at = json.indexOf("*/", i + 1) + 2; + if (at === 1) { + at = len; + } + i = at - 1; + } + ch = "\0"; + } else if (/[a-zA-Z$_]/.test(ch) && lastch !== ":") { + // Non-quoted identifier. Quote it. + ret += json.slice(at, i) + "\""; + at = i; + i = i + json.slice(i).search(/[^a-zA-Z0-9$_]|$/); + ret += json.slice(at, i) + "\""; + at = i; + } + + lastch = ch; + lasti = i; + } + + ret += json.slice(at); + if (hasMultipleParts) { + ret = "[" + ret + "]"; + } + + return ret; +} + +Json.isJSON = function (contentType, data) { + // Workaround for JSON responses without proper content type + // Let's consider all responses starting with "{" as JSON. In the worst + // case there will be an exception when parsing. This means that no-JSON + // responses (and post data) (with "{") can be parsed unnecessarily, + // which represents a little overhead, but this happens only if the request + // is actually expanded by the user in the UI (Net & Console panels). + // Do a manual string search instead of checking (data.strip()[0] === "{") + // to improve performance/memory usage. + let len = data ? data.length : 0; + for (let i = 0; i < len; i++) { + let ch = data.charAt(i); + if (ch === "{") { + return true; + } + + if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { + continue; + } + + break; + } + + if (!contentType) { + return false; + } + + contentType = contentType.split(";")[0]; + contentType = contentType.trim(); + return !!contentTypes[contentType]; +}; + +// Exports from this module +module.exports = Json; + diff --git a/devtools/client/webconsole/net/utils/moz.build b/devtools/client/webconsole/net/utils/moz.build new file mode 100644 index 0000000000..3fdc458e3f --- /dev/null +++ b/devtools/client/webconsole/net/utils/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'events.js', + 'json.js', + 'net.js', +) diff --git a/devtools/client/webconsole/net/utils/net.js b/devtools/client/webconsole/net/utils/net.js new file mode 100644 index 0000000000..782ec032a0 --- /dev/null +++ b/devtools/client/webconsole/net/utils/net.js @@ -0,0 +1,134 @@ +/* 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 mimeCategoryMap = { + "text/plain": "txt", + "application/octet-stream": "bin", + "text/html": "html", + "text/xml": "html", + "application/xml": "html", + "application/rss+xml": "html", + "application/atom+xml": "html", + "application/xhtml+xml": "html", + "application/mathml+xml": "html", + "application/rdf+xml": "html", + "text/css": "css", + "application/x-javascript": "js", + "text/javascript": "js", + "application/javascript": "js", + "text/ecmascript": "js", + "application/ecmascript": "js", + "image/jpeg": "image", + "image/jpg": "image", + "image/gif": "image", + "image/png": "image", + "image/bmp": "image", + "application/x-shockwave-flash": "plugin", + "application/x-silverlight-app": "plugin", + "video/x-flv": "media", + "audio/mpeg3": "media", + "audio/x-mpeg-3": "media", + "video/mpeg": "media", + "video/x-mpeg": "media", + "video/webm": "media", + "video/mp4": "media", + "video/ogg": "media", + "audio/ogg": "media", + "application/ogg": "media", + "application/x-ogg": "media", + "application/x-midi": "media", + "audio/midi": "media", + "audio/x-mid": "media", + "audio/x-midi": "media", + "music/crescendo": "media", + "audio/wav": "media", + "audio/x-wav": "media", + "application/x-woff": "font", + "application/font-woff": "font", + "application/x-font-woff": "font", + "application/x-ttf": "font", + "application/x-font-ttf": "font", + "font/ttf": "font", + "font/woff": "font", + "application/x-otf": "font", + "application/x-font-otf": "font" +}; + +var NetUtils = {}; + +NetUtils.isImage = function (contentType) { + if (!contentType) { + return false; + } + + contentType = contentType.split(";")[0]; + contentType = contentType.trim(); + return mimeCategoryMap[contentType] == "image"; +}; + +NetUtils.isHTML = function (contentType) { + if (!contentType) { + return false; + } + + contentType = contentType.split(";")[0]; + contentType = contentType.trim(); + return mimeCategoryMap[contentType] == "html"; +}; + +NetUtils.getHeaderValue = function (headers, name) { + if (!headers) { + return null; + } + + name = name.toLowerCase(); + for (let i = 0; i < headers.length; ++i) { + let headerName = headers[i].name.toLowerCase(); + if (headerName == name) { + return headers[i].value; + } + } +}; + +NetUtils.parseXml = function (content) { + let contentType = content.mimeType.split(";")[0]; + contentType = contentType.trim(); + + let parser = new DOMParser(); + let doc = parser.parseFromString(content.text, contentType); + let root = doc.documentElement; + + // Error handling + let nsURI = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; + if (root.namespaceURI == nsURI && root.nodeName == "parsererror") { + return null; + } + + return doc; +}; + +NetUtils.isURLEncodedRequest = function (file) { + let mimeType = "application/x-www-form-urlencoded"; + + let postData = file.request.postData; + if (postData && postData.text) { + let text = postData.text.toLowerCase(); + if (text.startsWith("content-type: " + mimeType)) { + return true; + } + } + + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + return value && value.startsWith(mimeType); +}; + +NetUtils.isMultiPartRequest = function (file) { + let mimeType = "multipart/form-data"; + let value = NetUtils.getHeaderValue(file.request.headers, "content-type"); + return value && value.startsWith(mimeType); +}; + +// Exports from this module +module.exports = NetUtils; diff --git a/devtools/client/webconsole/new-console-output/actions/enhancers.js b/devtools/client/webconsole/new-console-output/actions/enhancers.js new file mode 100644 index 0000000000..5553942e21 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/enhancers.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const { BATCH_ACTIONS } = require("../constants"); + +function batchActions(batchedActions) { + return { + type: BATCH_ACTIONS, + actions: batchedActions, + }; +} + +module.exports = { + batchActions +}; diff --git a/devtools/client/webconsole/new-console-output/actions/filters.js b/devtools/client/webconsole/new-console-output/actions/filters.js new file mode 100644 index 0000000000..05d0802191 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/filters.js @@ -0,0 +1,55 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters"); +const Services = require("Services"); + +const { + FILTER_TEXT_SET, + FILTER_TOGGLE, + FILTERS_CLEAR, + PREFS, +} = require("devtools/client/webconsole/new-console-output/constants"); + +function filterTextSet(text) { + return { + type: FILTER_TEXT_SET, + text + }; +} + +function filterToggle(filter) { + return (dispatch, getState) => { + dispatch({ + type: FILTER_TOGGLE, + filter, + }); + const filterState = getAllFilters(getState()); + Services.prefs.setBoolPref(PREFS.FILTER[filter.toUpperCase()], + filterState.get(filter)); + }; +} + +function filtersClear() { + return (dispatch, getState) => { + dispatch({ + type: FILTERS_CLEAR, + }); + + const filterState = getAllFilters(getState()); + for (let filter in filterState) { + Services.prefs.clearUserPref(PREFS.FILTER[filter.toUpperCase()]); + } + }; +} + +module.exports = { + filterTextSet, + filterToggle, + filtersClear +}; diff --git a/devtools/client/webconsole/new-console-output/actions/index.js b/devtools/client/webconsole/new-console-output/actions/index.js new file mode 100644 index 0000000000..5ce76a402a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/index.js @@ -0,0 +1,18 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const actionModules = [ + "enhancers", + "filters", + "messages", + "ui", +].map(filename => require(`./${filename}`)); + +const actions = Object.assign({}, ...actionModules); + +module.exports = actions; diff --git a/devtools/client/webconsole/new-console-output/actions/messages.js b/devtools/client/webconsole/new-console-output/actions/messages.js new file mode 100644 index 0000000000..467e275034 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/messages.js @@ -0,0 +1,100 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const { + prepareMessage +} = require("devtools/client/webconsole/new-console-output/utils/messages"); +const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator"); +const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers"); +const { + MESSAGE_ADD, + MESSAGES_CLEAR, + MESSAGE_OPEN, + MESSAGE_CLOSE, + MESSAGE_TYPE, + MESSAGE_TABLE_RECEIVE, +} = require("../constants"); + +const defaultIdGenerator = new IdGenerator(); + +function messageAdd(packet, idGenerator = null) { + if (idGenerator == null) { + idGenerator = defaultIdGenerator; + } + let message = prepareMessage(packet, idGenerator); + const addMessageAction = { + type: MESSAGE_ADD, + message + }; + + if (message.type === MESSAGE_TYPE.CLEAR) { + return batchActions([ + messagesClear(), + addMessageAction, + ]); + } + return addMessageAction; +} + +function messagesClear() { + return { + type: MESSAGES_CLEAR + }; +} + +function messageOpen(id) { + return { + type: MESSAGE_OPEN, + id + }; +} + +function messageClose(id) { + return { + type: MESSAGE_CLOSE, + id + }; +} + +function messageTableDataGet(id, client, dataType) { + return (dispatch) => { + let fetchObjectActorData; + if (["Map", "WeakMap", "Set", "WeakSet"].includes(dataType)) { + fetchObjectActorData = (cb) => client.enumEntries(cb); + } else { + fetchObjectActorData = (cb) => client.enumProperties({ + ignoreNonIndexedProperties: dataType === "Array" + }, cb); + } + + fetchObjectActorData(enumResponse => { + const {iterator} = enumResponse; + iterator.slice(0, iterator.count, sliceResponse => { + let {ownProperties} = sliceResponse; + dispatch(messageTableDataReceive(id, ownProperties)); + }); + }); + }; +} + +function messageTableDataReceive(id, data) { + return { + type: MESSAGE_TABLE_RECEIVE, + id, + data + }; +} + +module.exports = { + messageAdd, + messagesClear, + messageOpen, + messageClose, + messageTableDataGet, +}; + diff --git a/devtools/client/webconsole/new-console-output/actions/moz.build b/devtools/client/webconsole/new-console-output/actions/moz.build new file mode 100644 index 0000000000..c7a8ed52ca --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'enhancers.js', + 'filters.js', + 'index.js', + 'messages.js', + 'ui.js', +) diff --git a/devtools/client/webconsole/new-console-output/actions/ui.js b/devtools/client/webconsole/new-console-output/actions/ui.js new file mode 100644 index 0000000000..cf9814d793 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/actions/ui.js @@ -0,0 +1,27 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const Services = require("Services"); + +const { + FILTER_BAR_TOGGLE, + PREFS, +} = require("devtools/client/webconsole/new-console-output/constants"); + +function filterBarToggle(show) { + return (dispatch, getState) => { + dispatch({ + type: FILTER_BAR_TOGGLE + }); + const uiState = getAllUi(getState()); + Services.prefs.setBoolPref(PREFS.UI.FILTER_BAR, uiState.get("filterBarVisible")); + }; +} + +exports.filterBarToggle = filterBarToggle; diff --git a/devtools/client/webconsole/new-console-output/components/collapse-button.js b/devtools/client/webconsole/new-console-output/components/collapse-button.js new file mode 100644 index 0000000000..ab72fcf4d3 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createClass, + DOM: dom, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +const CollapseButton = createClass({ + + displayName: "CollapseButton", + + propTypes: { + open: PropTypes.bool.isRequired, + title: PropTypes.string, + }, + + getDefaultProps: function () { + return { + title: l10n.getStr("messageToggleDetails") + }; + }, + + render: function () { + const { open, onClick, title } = this.props; + + let classes = ["theme-twisty"]; + + if (open) { + classes.push("open"); + } + + return dom.a({ + className: classes.join(" "), + onClick, + title: title, + }); + } +}); + +module.exports = CollapseButton; diff --git a/devtools/client/webconsole/new-console-output/components/console-output.js b/devtools/client/webconsole/new-console-output/components/console-output.js new file mode 100644 index 0000000000..1ba7f8dda4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/console-output.js @@ -0,0 +1,125 @@ +/* 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 { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +const { + getAllMessages, + getAllMessagesUiById, + getAllMessagesTableDataById, + getAllGroupsById, +} = require("devtools/client/webconsole/new-console-output/selectors/messages"); +const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer); + +const ConsoleOutput = createClass({ + + displayName: "ConsoleOutput", + + propTypes: { + messages: PropTypes.object.isRequired, + messagesUi: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + attachRefToHud: PropTypes.func.isRequired, + }), + autoscroll: PropTypes.bool.isRequired, + }, + + componentDidMount() { + scrollToBottom(this.outputNode); + this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode); + }, + + componentWillUpdate(nextProps, nextState) { + if (!this.outputNode) { + return; + } + + const outputNode = this.outputNode; + + // Figure out if we are at the bottom. If so, then any new message should be scrolled + // into view. + if (this.props.autoscroll && outputNode.lastChild) { + this.shouldScrollBottom = isScrolledToBottom(outputNode.lastChild, outputNode); + } + }, + + componentDidUpdate() { + if (this.shouldScrollBottom) { + scrollToBottom(this.outputNode); + } + }, + + render() { + let { + dispatch, + autoscroll, + messages, + messagesUi, + messagesTableData, + serviceContainer, + groups, + } = this.props; + + let messageNodes = messages.map((message) => { + const parentGroups = message.groupId ? ( + (groups.get(message.groupId) || []) + .concat([message.groupId]) + ) : []; + + return ( + MessageContainer({ + dispatch, + message, + key: message.id, + serviceContainer, + open: messagesUi.includes(message.id), + tableData: messagesTableData.get(message.id), + autoscroll, + indent: parentGroups.length, + }) + ); + }); + return ( + dom.div({ + className: "webconsole-output", + ref: node => { + this.outputNode = node; + }, + }, messageNodes + ) + ); + } +}); + +function scrollToBottom(node) { + node.scrollTop = node.scrollHeight; +} + +function isScrolledToBottom(outputNode, scrollNode) { + let lastNodeHeight = outputNode.lastChild ? + outputNode.lastChild.clientHeight : 0; + return scrollNode.scrollTop + scrollNode.clientHeight >= + scrollNode.scrollHeight - lastNodeHeight / 2; +} + +function mapStateToProps(state, props) { + return { + messages: getAllMessages(state), + messagesUi: getAllMessagesUiById(state), + messagesTableData: getAllMessagesTableDataById(state), + autoscroll: getScrollSetting(state), + groups: getAllGroupsById(state), + }; +} + +module.exports = connect(mapStateToProps)(ConsoleOutput); diff --git a/devtools/client/webconsole/new-console-output/components/console-table.js b/devtools/client/webconsole/new-console-output/components/console-table.js new file mode 100644 index 0000000000..bf8fdcbd80 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/console-table.js @@ -0,0 +1,202 @@ +/* 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 { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { ObjectClient } = require("devtools/shared/client/main"); +const actions = require("devtools/client/webconsole/new-console-output/actions/messages"); +const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); + +const TABLE_ROW_MAX_ITEMS = 1000; +const TABLE_COLUMN_MAX_ITEMS = 10; + +const ConsoleTable = createClass({ + + displayName: "ConsoleTable", + + propTypes: { + dispatch: PropTypes.func.isRequired, + parameters: PropTypes.array.isRequired, + serviceContainer: PropTypes.shape({ + hudProxyClient: PropTypes.object.isRequired, + }), + id: PropTypes.string.isRequired, + }, + + componentWillMount: function () { + const {id, dispatch, serviceContainer, parameters} = this.props; + + if (!Array.isArray(parameters) || parameters.length === 0) { + return; + } + + const client = new ObjectClient(serviceContainer.hudProxyClient, parameters[0]); + let dataType = getParametersDataType(parameters); + + // Get all the object properties. + dispatch(actions.messageTableDataGet(id, client, dataType)); + }, + + getHeaders: function (columns) { + let headerItems = []; + columns.forEach((value, key) => headerItems.push(dom.th({}, value))); + return headerItems; + }, + + getRows: function (columns, items) { + return items.map(item => { + let cells = []; + columns.forEach((value, key) => { + cells.push( + dom.td( + {}, + GripMessageBody({ + grip: item[key] + }) + ) + ); + }); + return dom.tr({}, cells); + }); + }, + + render: function () { + const {parameters, tableData} = this.props; + const headersGrip = parameters[1]; + const headers = headersGrip && headersGrip.preview ? headersGrip.preview.items : null; + + // if tableData is nullable, we don't show anything. + if (!tableData) { + return null; + } + + const {columns, items} = getTableItems( + tableData, + getParametersDataType(parameters), + headers + ); + + return ( + dom.table({className: "new-consoletable devtools-monospace"}, + dom.thead({}, this.getHeaders(columns)), + dom.tbody({}, this.getRows(columns, items)) + ) + ); + } +}); + +function getParametersDataType(parameters = null) { + if (!Array.isArray(parameters) || parameters.length === 0) { + return null; + } + return parameters[0].class; +} + +function getTableItems(data = {}, type, headers = null) { + const INDEX_NAME = "_index"; + const VALUE_NAME = "_value"; + const namedIndexes = { + [INDEX_NAME]: ( + ["Object", "Array"].includes(type) ? + l10n.getStr("table.index") : l10n.getStr("table.iterationIndex") + ), + [VALUE_NAME]: l10n.getStr("table.value"), + key: l10n.getStr("table.key") + }; + + let columns = new Map(); + let items = []; + + let addItem = function (item) { + items.push(item); + Object.keys(item).forEach(key => addColumn(key)); + }; + + let addColumn = function (columnIndex) { + let columnExists = columns.has(columnIndex); + let hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS; + let hasCustomHeaders = Array.isArray(headers); + + if ( + !columnExists && + !hasMaxColumns && ( + !hasCustomHeaders || + headers.includes(columnIndex) || + columnIndex === INDEX_NAME + ) + ) { + columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex); + } + }; + + for (let index of Object.keys(data)) { + if (type !== "Object" && index == parseInt(index, 10)) { + index = parseInt(index, 10); + } + + let item = { + [INDEX_NAME]: index + }; + + let property = data[index].value; + + if (property.preview) { + let {preview} = property; + let entries = preview.ownProperties || preview.items; + if (entries) { + for (let key of Object.keys(entries)) { + let entry = entries[key]; + item[key] = entry.value || entry; + } + } else { + if (preview.key) { + item.key = preview.key; + } + + item[VALUE_NAME] = preview.value || property; + } + } else { + item[VALUE_NAME] = property; + } + + addItem(item); + + if (items.length === TABLE_ROW_MAX_ITEMS) { + break; + } + } + + // Some headers might not be present in the items, so we make sure to + // return all the headers set by the user. + if (Array.isArray(headers)) { + headers.forEach(header => addColumn(header)); + } + + // We want to always have the index column first + if (columns.has(INDEX_NAME)) { + let index = columns.get(INDEX_NAME); + columns.delete(INDEX_NAME); + columns = new Map([[INDEX_NAME, index], ...columns.entries()]); + } + + // We want to always have the values column last + if (columns.has(VALUE_NAME)) { + let index = columns.get(VALUE_NAME); + columns.delete(VALUE_NAME); + columns.set(VALUE_NAME, index); + } + + return { + columns, + items + }; +} + +module.exports = ConsoleTable; diff --git a/devtools/client/webconsole/new-console-output/components/filter-bar.js b/devtools/client/webconsole/new-console-output/components/filter-bar.js new file mode 100644 index 0000000000..a386a414a9 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js @@ -0,0 +1,170 @@ +/* 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 { + createFactory, + createClass, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters"); +const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index"); +const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index"); +const uiActions = require("devtools/client/webconsole/new-console-output/actions/index"); +const { + MESSAGE_LEVEL +} = require("../constants"); +const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button")); + +const FilterBar = createClass({ + + displayName: "FilterBar", + + propTypes: { + filter: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + attachRefToHud: PropTypes.func.isRequired, + }).isRequired, + ui: PropTypes.object.isRequired + }, + + componentDidMount() { + this.props.serviceContainer.attachRefToHud("filterBox", + this.wrapperNode.querySelector(".text-filter")); + }, + + onClickMessagesClear: function () { + this.props.dispatch(messagesClear()); + }, + + onClickFilterBarToggle: function () { + this.props.dispatch(uiActions.filterBarToggle()); + }, + + onClickFiltersClear: function () { + this.props.dispatch(filtersClear()); + }, + + onSearchInput: function (e) { + this.props.dispatch(filterTextSet(e.target.value)); + }, + + render() { + const {dispatch, filter, ui} = this.props; + let filterBarVisible = ui.filterBarVisible; + let children = []; + + children.push(dom.div({className: "devtools-toolbar webconsole-filterbar-primary"}, + dom.button({ + className: "devtools-button devtools-clear-icon", + title: "Clear output", + onClick: this.onClickMessagesClear + }), + dom.button({ + className: "devtools-button devtools-filter-icon" + ( + filterBarVisible ? " checked" : ""), + title: "Toggle filter bar", + onClick: this.onClickFilterBarToggle + }), + dom.input({ + className: "devtools-plaininput text-filter", + type: "search", + value: filter.text, + placeholder: "Filter output", + onInput: this.onSearchInput + }) + )); + + if (filterBarVisible) { + children.push( + dom.div({className: "devtools-toolbar webconsole-filterbar-secondary"}, + FilterButton({ + active: filter.error, + label: "Errors", + filterKey: MESSAGE_LEVEL.ERROR, + dispatch + }), + FilterButton({ + active: filter.warn, + label: "Warnings", + filterKey: MESSAGE_LEVEL.WARN, + dispatch + }), + FilterButton({ + active: filter.log, + label: "Logs", + filterKey: MESSAGE_LEVEL.LOG, + dispatch + }), + FilterButton({ + active: filter.info, + label: "Info", + filterKey: MESSAGE_LEVEL.INFO, + dispatch + }), + FilterButton({ + active: filter.debug, + label: "Debug", + filterKey: MESSAGE_LEVEL.DEBUG, + dispatch + }), + dom.span({ + className: "devtools-separator", + }), + FilterButton({ + active: filter.netxhr, + label: "XHR", + filterKey: "netxhr", + dispatch + }), + FilterButton({ + active: filter.net, + label: "Requests", + filterKey: "net", + dispatch + }) + ) + ); + } + + if (ui.filteredMessageVisible) { + children.push( + dom.div({className: "devtools-toolbar"}, + dom.span({ + className: "clear"}, + "You have filters set that may hide some results. " + + "Learn more about our filtering syntax ", + dom.a({}, "here"), + "."), + dom.button({ + className: "menu-filter-button", + onClick: this.onClickFiltersClear + }, "Remove filters") + ) + ); + } + + return ( + dom.div({ + className: "webconsole-filteringbar-wrapper", + ref: node => { + this.wrapperNode = node; + } + }, ...children + ) + ); + } +}); + +function mapStateToProps(state) { + return { + filter: getAllFilters(state), + ui: getAllUi(state) + }; +} + +module.exports = connect(mapStateToProps)(FilterBar); diff --git a/devtools/client/webconsole/new-console-output/components/filter-button.js b/devtools/client/webconsole/new-console-output/components/filter-button.js new file mode 100644 index 0000000000..4116bb5248 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/filter-button.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/. */ +"use strict"; + +const { + createClass, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); + +const FilterButton = createClass({ + + displayName: "FilterButton", + + propTypes: { + label: PropTypes.string.isRequired, + filterKey: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired, + dispatch: PropTypes.func.isRequired, + }, + + onClick: function () { + this.props.dispatch(actions.filterToggle(this.props.filterKey)); + }, + + render() { + const {active, label, filterKey} = this.props; + + let classList = [ + "menu-filter-button", + filterKey, + ]; + if (active) { + classList.push("checked"); + } + + return dom.button({ + className: classList.join(" "), + onClick: this.onClick + }, label); + } +}); + +module.exports = FilterButton; diff --git a/devtools/client/webconsole/new-console-output/components/grip-message-body.js b/devtools/client/webconsole/new-console-output/components/grip-message-body.js new file mode 100644 index 0000000000..29c2e6a4f2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// If this is being run from Mocha, then the browser loader hasn't set up +// define. We need to do that before loading Rep. +if (typeof define === "undefined") { + require("amd-loader"); +} + +// React +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); +const StringRep = createFactories(require("devtools/client/shared/components/reps/string").StringRep).rep; +const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link")); +const { Grip } = require("devtools/client/shared/components/reps/grip"); + +GripMessageBody.displayName = "GripMessageBody"; + +GripMessageBody.propTypes = { + grip: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.object, + ]).isRequired, + serviceContainer: PropTypes.shape({ + createElement: PropTypes.func.isRequired, + }), + userProvidedStyle: PropTypes.string, +}; + +function GripMessageBody(props) { + const { grip, userProvidedStyle, serviceContainer } = props; + + let styleObject; + if (userProvidedStyle && userProvidedStyle !== "") { + styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement); + } + + return ( + // @TODO once there is a longString rep, also turn off quotes for those. + typeof grip === "string" + ? StringRep({ + object: grip, + useQuotes: false, + mode: props.mode, + style: styleObject + }) + : Rep({ + object: grip, + objectLink: VariablesViewLink, + defaultRep: Grip, + mode: props.mode, + }) + ); +} + +function cleanupStyle(userProvidedStyle, createElement) { + // Regular expression that matches the allowed CSS property names. + const allowedStylesRegex = new RegExp( + "^(?:-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 expression that matches the forbidden CSS property values. + const forbiddenValuesRegexs = [ + // url(), -moz-element() + /\b(?:url|(?:-moz-)?element)[\s('"]+/gi, + + // various URL protocols + /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi, + ]; + + // Use a dummy element to parse the style string. + let dummy = createElement("div"); + dummy.style = userProvidedStyle; + + // Return a style object as expected by React DOM components, e.g. + // {color: "red"} + // without forbidden properties and values. + return [...dummy.style] + .filter(name => { + return allowedStylesRegex.test(name) + && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name])); + }) + .reduce((object, name) => { + return Object.assign({ + [name]: dummy.style[name] + }, object); + }, {}); +} + +module.exports = GripMessageBody; diff --git a/devtools/client/webconsole/new-console-output/components/message-container.js b/devtools/client/webconsole/new-console-output/components/message-container.js new file mode 100644 index 0000000000..115e9e2915 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-container.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createClass, + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); + +const { + MESSAGE_SOURCE, + MESSAGE_TYPE +} = require("devtools/client/webconsole/new-console-output/constants"); + +const componentMap = new Map([ + ["ConsoleApiCall", require("./message-types/console-api-call")], + ["ConsoleCommand", require("./message-types/console-command")], + ["DefaultRenderer", require("./message-types/default-renderer")], + ["EvaluationResult", require("./message-types/evaluation-result")], + ["NetworkEventMessage", require("./message-types/network-event-message")], + ["PageError", require("./message-types/page-error")] +]); + +const MessageContainer = createClass({ + displayName: "MessageContainer", + + propTypes: { + message: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + serviceContainer: PropTypes.object.isRequired, + autoscroll: PropTypes.bool.isRequired, + indent: PropTypes.number.isRequired, + }, + + getDefaultProps: function () { + return { + open: false, + indent: 0, + }; + }, + + shouldComponentUpdate(nextProps, nextState) { + const repeatChanged = this.props.message.repeat !== nextProps.message.repeat; + const openChanged = this.props.open !== nextProps.open; + const tableDataChanged = this.props.tableData !== nextProps.tableData; + return repeatChanged || openChanged || tableDataChanged; + }, + + render() { + const { message } = this.props; + + let MessageComponent = createFactory(getMessageComponent(message)); + return MessageComponent(this.props); + } +}); + +function getMessageComponent(message) { + switch (message.source) { + case MESSAGE_SOURCE.CONSOLE_API: + return componentMap.get("ConsoleApiCall"); + case MESSAGE_SOURCE.NETWORK: + return componentMap.get("NetworkEventMessage"); + case MESSAGE_SOURCE.JAVASCRIPT: + switch (message.type) { + case MESSAGE_TYPE.COMMAND: + return componentMap.get("ConsoleCommand"); + case MESSAGE_TYPE.RESULT: + return componentMap.get("EvaluationResult"); + // @TODO this is probably not the right behavior, but works for now. + // Chrome doesn't distinguish between page errors and log messages. We + // may want to remove the PageError component and just handle errors + // with ConsoleApiCall. + case MESSAGE_TYPE.LOG: + return componentMap.get("PageError"); + default: + return componentMap.get("DefaultRenderer"); + } + } + + return componentMap.get("DefaultRenderer"); +} + +module.exports.MessageContainer = MessageContainer; + +// Exported so we can test it with unit tests. +module.exports.getMessageComponent = getMessageComponent; diff --git a/devtools/client/webconsole/new-console-output/components/message-icon.js b/devtools/client/webconsole/new-console-output/components/message-icon.js new file mode 100644 index 0000000000..b4c32fda0a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-icon.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); + +MessageIcon.displayName = "MessageIcon"; + +MessageIcon.propTypes = { + level: PropTypes.string.isRequired, +}; + +function MessageIcon(props) { + const { level } = props; + + const title = l10n.getStr("level." + level); + return dom.div({ + className: "icon", + title + }); +} + +module.exports = MessageIcon; diff --git a/devtools/client/webconsole/new-console-output/components/message-indent.js b/devtools/client/webconsole/new-console-output/components/message-indent.js new file mode 100644 index 0000000000..354e135897 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-indent.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createClass, + DOM: dom, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const INDENT_WIDTH = 12; +const MessageIndent = createClass({ + + displayName: "MessageIndent", + + propTypes: { + indent: PropTypes.number.isRequired, + }, + + render: function () { + const { indent } = this.props; + return dom.span({ + className: "indent", + style: {"width": indent * INDENT_WIDTH} + }); + } +}); + +module.exports.MessageIndent = MessageIndent; + +// Exported so we can test it with unit tests. +module.exports.INDENT_WIDTH = INDENT_WIDTH; diff --git a/devtools/client/webconsole/new-console-output/components/message-repeat.js b/devtools/client/webconsole/new-console-output/components/message-repeat.js new file mode 100644 index 0000000000..1820340ea3 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js @@ -0,0 +1,36 @@ + +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +MessageRepeat.displayName = "MessageRepeat"; + +MessageRepeat.propTypes = { + repeat: PropTypes.number.isRequired +}; + +function MessageRepeat(props) { + const { repeat } = props; + const visibility = repeat > 1 ? "visible" : "hidden"; + + return dom.span({ + className: "message-repeats", + style: {visibility}, + title: PluralForm.get(repeat, l10n.getStr("messageRepeats.tooltip2")) + .replace("#1", repeat) + }, repeat); +} + +module.exports = MessageRepeat; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js new file mode 100644 index 0000000000..7200648fa5 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js @@ -0,0 +1,132 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); +const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table")); +const {isGroupType, l10n} = require("devtools/client/webconsole/new-console-output/utils/messages"); + +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +ConsoleApiCall.displayName = "ConsoleApiCall"; + +ConsoleApiCall.propTypes = { + message: PropTypes.object.isRequired, + open: PropTypes.bool, + serviceContainer: PropTypes.object.isRequired, + indent: PropTypes.number.isRequired, +}; + +ConsoleApiCall.defaultProps = { + open: false, + indent: 0, +}; + +function ConsoleApiCall(props) { + const { + dispatch, + message, + open, + tableData, + serviceContainer, + indent, + } = props; + const { + id: messageId, + source, + type, + level, + repeat, + stacktrace, + frame, + parameters, + messageText, + userProvidedStyles, + } = message; + + let messageBody; + if (type === "trace") { + messageBody = dom.span({className: "cm-variable"}, "console.trace()"); + } else if (type === "assert") { + let reps = formatReps(parameters); + messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps); + } else if (type === "table") { + // TODO: Chrome does not output anything, see if we want to keep this + messageBody = dom.span({className: "cm-variable"}, "console.table()"); + } else if (parameters) { + messageBody = formatReps(parameters, userProvidedStyles, serviceContainer); + } else { + messageBody = messageText; + } + + let attachment = null; + if (type === "table") { + attachment = ConsoleTable({ + dispatch, + id: message.id, + serviceContainer, + parameters: message.parameters, + tableData + }); + } + + let collapseTitle = null; + if (isGroupType(type)) { + collapseTitle = l10n.getStr("groupToggle"); + } + + const collapsible = isGroupType(type) + || (type === "error" && Array.isArray(stacktrace)); + const topLevelClasses = ["cm-s-mozilla"]; + + return Message({ + messageId, + open, + collapsible, + collapseTitle, + source, + type, + level, + topLevelClasses, + messageBody, + repeat, + frame, + stacktrace, + attachment, + serviceContainer, + dispatch, + indent, + }); +} + +function formatReps(parameters, userProvidedStyles, serviceContainer) { + return ( + parameters + // Get all the grips. + .map((grip, key) => GripMessageBody({ + grip, + key, + userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null, + serviceContainer + })) + // Interleave spaces. + .reduce((arr, v, i) => { + return i + 1 < parameters.length + ? arr.concat(v, dom.span({}, " ")) + : arr.concat(v); + }, []) + ); +} + +module.exports = ConsoleApiCall; + diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js new file mode 100644 index 0000000000..d87229fa94 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +ConsoleCommand.displayName = "ConsoleCommand"; + +ConsoleCommand.propTypes = { + message: PropTypes.object.isRequired, + autoscroll: PropTypes.bool.isRequired, + indent: PropTypes.number.isRequired, +}; + +ConsoleCommand.defaultProps = { + indent: 0, +}; + +/** + * Displays input from the console. + */ +function ConsoleCommand(props) { + const { autoscroll, indent, message } = props; + const { + source, + type, + level, + messageText: messageBody, + } = message; + + const { + serviceContainer, + } = props; + + const childProps = { + source, + type, + level, + topLevelClasses: [], + messageBody, + scrollToMessage: autoscroll, + serviceContainer, + indent: indent, + }; + return Message(childProps); +} + +module.exports = ConsoleCommand; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js new file mode 100644 index 0000000000..d07089531e --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js @@ -0,0 +1,22 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + DOM: dom, +} = require("devtools/client/shared/vendor/react"); + +DefaultRenderer.displayName = "DefaultRenderer"; + +function DefaultRenderer(props) { + return dom.div({}, + "This message type is not supported yet." + ); +} + +module.exports = DefaultRenderer; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js new file mode 100644 index 0000000000..992dc62cfd --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); +const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body")); + +EvaluationResult.displayName = "EvaluationResult"; + +EvaluationResult.propTypes = { + message: PropTypes.object.isRequired, + indent: PropTypes.number.isRequired, +}; + +EvaluationResult.defaultProps = { + indent: 0, +}; + +function EvaluationResult(props) { + const { message, serviceContainer, indent } = props; + const { + source, + type, + level, + id: messageId, + exceptionDocURL, + frame, + } = message; + + let messageBody; + if (message.messageText) { + messageBody = message.messageText; + } else { + messageBody = GripMessageBody({grip: message.parameters}); + } + + const topLevelClasses = ["cm-s-mozilla"]; + + const childProps = { + source, + type, + level, + indent, + topLevelClasses, + messageBody, + messageId, + scrollToMessage: props.autoscroll, + serviceContainer, + exceptionDocURL, + frame, + }; + return Message(childProps); +} + +module.exports = EvaluationResult; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/moz.build b/devtools/client/webconsole/new-console-output/components/message-types/moz.build new file mode 100644 index 0000000000..9b9f720171 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/moz.build @@ -0,0 +1,13 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'console-api-call.js', + 'console-command.js', + 'default-renderer.js', + 'evaluation-result.js', + 'network-event-message.js', + 'page-error.js', +) diff --git a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js new file mode 100644 index 0000000000..e3c81a4874 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +NetworkEventMessage.displayName = "NetworkEventMessage"; + +NetworkEventMessage.propTypes = { + message: PropTypes.object.isRequired, + serviceContainer: PropTypes.shape({ + openNetworkPanel: PropTypes.func.isRequired, + }), + indent: PropTypes.number.isRequired, +}; + +NetworkEventMessage.defaultProps = { + indent: 0, +}; + +function NetworkEventMessage(props) { + const { message, serviceContainer, indent } = props; + const { actor, source, type, level, request, isXHR } = message; + + const topLevelClasses = [ "cm-s-mozilla" ]; + + function onUrlClick() { + serviceContainer.openNetworkPanel(actor); + } + + const method = dom.span({className: "method" }, request.method); + const xhr = isXHR + ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator")) + : null; + const url = dom.a({ className: "url", title: request.url, onClick: onUrlClick }, + request.url.replace(/\?.+/, "")); + + const messageBody = dom.span({}, method, xhr, url); + + const childProps = { + source, + type, + level, + indent, + topLevelClasses, + messageBody, + serviceContainer, + }; + return Message(childProps); +} + +module.exports = NetworkEventMessage; diff --git a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js new file mode 100644 index 0000000000..77ea75ff74 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createFactory, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message")); + +PageError.displayName = "PageError"; + +PageError.propTypes = { + message: PropTypes.object.isRequired, + open: PropTypes.bool, + indent: PropTypes.number.isRequired, +}; + +PageError.defaultProps = { + open: false, + indent: 0, +}; + +function PageError(props) { + const { + dispatch, + message, + open, + serviceContainer, + indent, + } = props; + const { + id: messageId, + source, + type, + level, + messageText: messageBody, + repeat, + stacktrace, + frame, + exceptionDocURL, + } = message; + + const childProps = { + dispatch, + messageId, + open, + collapsible: Array.isArray(stacktrace), + source, + type, + level, + topLevelClasses: [], + indent, + messageBody, + repeat, + frame, + stacktrace, + serviceContainer, + exceptionDocURL, + }; + return Message(childProps); +} + +module.exports = PageError; diff --git a/devtools/client/webconsole/new-console-output/components/message.js b/devtools/client/webconsole/new-console-output/components/message.js new file mode 100644 index 0000000000..f36bff7e4a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/message.js @@ -0,0 +1,176 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + createClass, + createFactory, + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); +const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button")); +const MessageIndent = createFactory(require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent); +const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon")); +const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat")); +const FrameView = createFactory(require("devtools/client/shared/components/frame")); +const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace")); + +const Message = createClass({ + displayName: "Message", + + propTypes: { + open: PropTypes.bool, + collapsible: PropTypes.bool, + collapseTitle: PropTypes.string, + source: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + level: PropTypes.string.isRequired, + indent: PropTypes.number.isRequired, + topLevelClasses: PropTypes.array.isRequired, + messageBody: PropTypes.any.isRequired, + repeat: PropTypes.any, + frame: PropTypes.any, + attachment: PropTypes.any, + stacktrace: PropTypes.any, + messageId: PropTypes.string, + scrollToMessage: PropTypes.bool, + exceptionDocURL: PropTypes.string, + serviceContainer: PropTypes.shape({ + emitNewMessage: PropTypes.func.isRequired, + onViewSourceInDebugger: PropTypes.func.isRequired, + sourceMapService: PropTypes.any, + }), + }, + + getDefaultProps: function () { + return { + indent: 0 + }; + }, + + componentDidMount() { + if (this.messageNode) { + if (this.props.scrollToMessage) { + this.messageNode.scrollIntoView(); + } + // Event used in tests. Some message types don't pass it in because existing tests + // did not emit for them. + if (this.props.serviceContainer) { + this.props.serviceContainer.emitNewMessage(this.messageNode, this.props.messageId); + } + } + }, + + onLearnMoreClick: function () { + let {exceptionDocURL} = this.props; + this.props.serviceContainer.openLink(exceptionDocURL); + }, + + render() { + const { + messageId, + open, + collapsible, + collapseTitle, + source, + type, + level, + indent, + topLevelClasses, + messageBody, + frame, + stacktrace, + serviceContainer, + dispatch, + exceptionDocURL, + } = this.props; + + topLevelClasses.push("message", source, type, level); + if (open) { + topLevelClasses.push("open"); + } + + const icon = MessageIcon({level}); + + // Figure out if there is an expandable part to the message. + let attachment = null; + if (this.props.attachment) { + attachment = this.props.attachment; + } else if (stacktrace) { + const child = open ? StackTrace({ + stacktrace: stacktrace, + onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger + }) : null; + attachment = dom.div({ className: "stacktrace devtools-monospace" }, child); + } + + // If there is an expandable part, make it collapsible. + let collapse = null; + if (collapsible) { + collapse = CollapseButton({ + open, + title: collapseTitle, + onClick: function () { + if (open) { + dispatch(actions.messageClose(messageId)); + } else { + dispatch(actions.messageOpen(messageId)); + } + }, + }); + } + + const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null; + + // Configure the location. + const location = dom.span({ className: "message-location devtools-monospace" }, + frame ? FrameView({ + frame, + onClick: serviceContainer ? serviceContainer.onViewSourceInDebugger : undefined, + showEmptyPathAsHost: true, + sourceMapService: serviceContainer ? serviceContainer.sourceMapService : undefined + }) : null + ); + + let learnMore; + if (exceptionDocURL) { + learnMore = dom.a({ + className: "learn-more-link webconsole-learn-more-link", + title: exceptionDocURL.split("?")[0], + onClick: this.onLearnMoreClick, + }, `[${l10n.getStr("webConsoleMoreInfoLabel")}]`); + } + + return dom.div({ + className: topLevelClasses.join(" "), + ref: node => { + this.messageNode = node; + } + }, + // @TODO add timestamp + MessageIndent({indent}), + icon, + collapse, + dom.span({ className: "message-body-wrapper" }, + dom.span({ className: "message-flex-body" }, + dom.span({ className: "message-body devtools-monospace" }, + messageBody, + learnMore + ), + repeat, + location + ), + attachment + ) + ); + } +}); + +module.exports = Message; diff --git a/devtools/client/webconsole/new-console-output/components/moz.build b/devtools/client/webconsole/new-console-output/components/moz.build new file mode 100644 index 0000000000..8c00223147 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/moz.build @@ -0,0 +1,23 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'message-types' +] + +DevToolsModules( + 'collapse-button.js', + 'console-output.js', + 'console-table.js', + 'filter-bar.js', + 'filter-button.js', + 'grip-message-body.js', + 'message-container.js', + 'message-icon.js', + 'message-indent.js', + 'message-repeat.js', + 'message.js', + 'variables-view-link.js' +) diff --git a/devtools/client/webconsole/new-console-output/components/variables-view-link.js b/devtools/client/webconsole/new-console-output/components/variables-view-link.js new file mode 100644 index 0000000000..4d79c322f0 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +// React & Redux +const { + DOM: dom, + PropTypes +} = require("devtools/client/shared/vendor/react"); +const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view"); + +VariablesViewLink.displayName = "VariablesViewLink"; + +VariablesViewLink.propTypes = { + object: PropTypes.object.isRequired +}; + +function VariablesViewLink(props) { + const { object, children } = props; + + return ( + dom.a({ + onClick: openVariablesView.bind(null, object), + className: "cm-variable", + draggable: false, + }, children) + ); +} + +module.exports = VariablesViewLink; diff --git a/devtools/client/webconsole/new-console-output/constants.js b/devtools/client/webconsole/new-console-output/constants.js new file mode 100644 index 0000000000..ef11d6eb8e --- /dev/null +++ b/devtools/client/webconsole/new-console-output/constants.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const actionTypes = { + BATCH_ACTIONS: "BATCH_ACTIONS", + MESSAGE_ADD: "MESSAGE_ADD", + MESSAGES_CLEAR: "MESSAGES_CLEAR", + MESSAGE_OPEN: "MESSAGE_OPEN", + MESSAGE_CLOSE: "MESSAGE_CLOSE", + MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE", + FILTER_TOGGLE: "FILTER_TOGGLE", + FILTER_TEXT_SET: "FILTER_TEXT_SET", + FILTERS_CLEAR: "FILTERS_CLEAR", + FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE", +}; + +const prefs = { + PREFS: { + FILTER: { + ERROR: "devtools.webconsole.filter.error", + WARN: "devtools.webconsole.filter.warn", + INFO: "devtools.webconsole.filter.info", + LOG: "devtools.webconsole.filter.log", + DEBUG: "devtools.webconsole.filter.debug", + NET: "devtools.webconsole.filter.net", + NETXHR: "devtools.webconsole.filter.netxhr", + }, + UI: { + FILTER_BAR: "devtools.webconsole.ui.filterbar" + } + } +}; + +const chromeRDPEnums = { + MESSAGE_SOURCE: { + XML: "xml", + JAVASCRIPT: "javascript", + NETWORK: "network", + CONSOLE_API: "console-api", + STORAGE: "storage", + APPCACHE: "appcache", + RENDERING: "rendering", + SECURITY: "security", + OTHER: "other", + DEPRECATION: "deprecation" + }, + MESSAGE_TYPE: { + LOG: "log", + DIR: "dir", + TABLE: "table", + TRACE: "trace", + CLEAR: "clear", + START_GROUP: "startGroup", + START_GROUP_COLLAPSED: "startGroupCollapsed", + END_GROUP: "endGroup", + ASSERT: "assert", + PROFILE: "profile", + PROFILE_END: "profileEnd", + // Undocumented in Chrome RDP, but is used for evaluation results. + RESULT: "result", + // Undocumented in Chrome RDP, but is used for input. + COMMAND: "command", + // Undocumented in Chrome RDP, but is used for messages that should not + // output anything (e.g. `console.time()` calls). + NULL_MESSAGE: "nullMessage", + }, + MESSAGE_LEVEL: { + LOG: "log", + ERROR: "error", + WARN: "warn", + DEBUG: "debug", + INFO: "info" + } +}; + +// Combine into a single constants object +module.exports = Object.assign({}, actionTypes, prefs, chromeRDPEnums); diff --git a/devtools/client/webconsole/new-console-output/main.js b/devtools/client/webconsole/new-console-output/main.js new file mode 100644 index 0000000000..29db5e3378 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/main.js @@ -0,0 +1,23 @@ +/* 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/. */ + + /* global BrowserLoader */ + +"use strict"; + +var { utils: Cu } = Components; + +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); +const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); + +// Initialize module loader and load all modules of the new inline +// preview feature. The entire code-base doesn't need any extra +// privileges and runs entirely in content scope. +const NewConsoleOutputWrapper = BrowserLoader({ + baseURI: "resource://devtools/client/webconsole/new-console-output/", + window}).require("./new-console-output-wrapper"); + +this.NewConsoleOutput = function (parentNode, jsterm, toolbox, owner, serviceContainer) { + return new NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, serviceContainer); +}; diff --git a/devtools/client/webconsole/new-console-output/moz.build b/devtools/client/webconsole/new-console-output/moz.build new file mode 100644 index 0000000000..7d0905aaa4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/moz.build @@ -0,0 +1,21 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'actions', + 'components', + 'reducers', + 'selectors', + 'test', + 'utils', +] + +DevToolsModules( + 'constants.js', + 'main.js', + 'new-console-output-wrapper.js', + 'store.js', + 'types.js', +) diff --git a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js new file mode 100644 index 0000000000..17c1e767d7 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js @@ -0,0 +1,134 @@ +/* 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"; + +// React & Redux +const React = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); +const { Provider } = require("devtools/client/shared/vendor/react-redux"); + +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); +const { configureStore } = require("devtools/client/webconsole/new-console-output/store"); + +const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output")); +const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar")); + +const store = configureStore(); +let queuedActions = []; +let throttledDispatchTimeout = false; + +function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) { + this.parentNode = parentNode; + this.jsterm = jsterm; + this.toolbox = toolbox; + this.owner = owner; + this.document = document; + + this.init = this.init.bind(this); +} + +NewConsoleOutputWrapper.prototype = { + init: function () { + const attachRefToHud = (id, node) => { + this.jsterm.hud[id] = node; + }; + + let childComponent = ConsoleOutput({ + serviceContainer: { + attachRefToHud, + emitNewMessage: (node, messageId) => { + this.jsterm.hud.emit("new-messages", new Set([{ + node, + messageId, + }])); + }, + hudProxyClient: this.jsterm.hud.proxy.client, + onViewSourceInDebugger: frame => this.toolbox.viewSourceInDebugger.call( + this.toolbox, + frame.url, + frame.line + ), + openNetworkPanel: (requestId) => { + return this.toolbox.selectTool("netmonitor").then(panel => { + return panel.panelWin.NetMonitorController.inspectRequest(requestId); + }); + }, + sourceMapService: this.toolbox ? this.toolbox._sourceMapService : null, + openLink: url => this.jsterm.hud.owner.openLink.call(this.jsterm.hud.owner, url), + createElement: nodename => { + return this.document.createElementNS("http://www.w3.org/1999/xhtml", nodename); + } + } + }); + let filterBar = FilterBar({ + serviceContainer: { + attachRefToHud + } + }); + let provider = React.createElement( + Provider, + { store }, + React.DOM.div( + {className: "webconsole-output-wrapper"}, + filterBar, + childComponent + )); + + this.body = ReactDOM.render(provider, this.parentNode); + }, + + dispatchMessageAdd: function (message, waitForResponse) { + let action = actions.messageAdd(message); + batchedMessageAdd(action); + + // Wait for the message to render to resolve with the DOM node. + // This is just for backwards compatibility with old tests, and should + // be removed once it's not needed anymore. + // Can only wait for response if the action contains a valid message. + if (waitForResponse && action.message) { + let messageId = action.message.get("id"); + return new Promise(resolve => { + let jsterm = this.jsterm; + jsterm.hud.on("new-messages", function onThisMessage(e, messages) { + for (let m of messages) { + if (m.messageId == messageId) { + resolve(m.node); + jsterm.hud.off("new-messages", onThisMessage); + return; + } + } + }); + }); + } + + return Promise.resolve(); + }, + + dispatchMessagesAdd: function (messages) { + const batchedActions = messages.map(message => actions.messageAdd(message)); + store.dispatch(actions.batchActions(batchedActions)); + }, + + dispatchMessagesClear: function () { + store.dispatch(actions.messagesClear()); + }, + // Should be used for test purpose only. + getStore: function () { + return store; + } +}; + +function batchedMessageAdd(action) { + queuedActions.push(action); + if (!throttledDispatchTimeout) { + throttledDispatchTimeout = setTimeout(() => { + store.dispatch(actions.batchActions(queuedActions)); + queuedActions = []; + throttledDispatchTimeout = null; + }, 50); + } +} + +// Exports from this module +module.exports = NewConsoleOutputWrapper; diff --git a/devtools/client/webconsole/new-console-output/reducers/filters.js b/devtools/client/webconsole/new-console-output/reducers/filters.js new file mode 100644 index 0000000000..cd5f4bf7c4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/filters.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const Immutable = require("devtools/client/shared/vendor/immutable"); +const constants = require("devtools/client/webconsole/new-console-output/constants"); + +const FilterState = Immutable.Record({ + debug: true, + error: true, + info: true, + log: true, + net: false, + netxhr: false, + text: "", + warn: true, +}); + +function filters(state = new FilterState(), action) { + switch (action.type) { + case constants.FILTER_TOGGLE: + const {filter} = action; + const active = !state.get(filter); + return state.set(filter, active); + case constants.FILTERS_CLEAR: + return new FilterState(); + case constants.FILTER_TEXT_SET: + let {text} = action; + return state.set("text", text); + } + + return state; +} + +exports.FilterState = FilterState; +exports.filters = filters; diff --git a/devtools/client/webconsole/new-console-output/reducers/index.js b/devtools/client/webconsole/new-console-output/reducers/index.js new file mode 100644 index 0000000000..6ab10d565f --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/index.js @@ -0,0 +1,18 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const { filters } = require("./filters"); +const { messages } = require("./messages"); +const { prefs } = require("./prefs"); +const { ui } = require("./ui"); + +exports.reducers = { + filters, + messages, + prefs, + ui, +}; diff --git a/devtools/client/webconsole/new-console-output/reducers/messages.js b/devtools/client/webconsole/new-console-output/reducers/messages.js new file mode 100644 index 0000000000..0693fed608 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/messages.js @@ -0,0 +1,135 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const Immutable = require("devtools/client/shared/vendor/immutable"); +const constants = require("devtools/client/webconsole/new-console-output/constants"); +const {isGroupType} = require("devtools/client/webconsole/new-console-output/utils/messages"); + +const MessageState = Immutable.Record({ + // List of all the messages added to the console. + messagesById: Immutable.List(), + // List of the message ids which are opened. + messagesUiById: Immutable.List(), + // Map of the form {messageId : tableData}, which represent the data passed + // as an argument in console.table calls. + messagesTableDataById: Immutable.Map(), + // Map of the form {groupMessageId : groupArray}, + // where groupArray is the list of of all the parent groups' ids of the groupMessageId. + groupsById: Immutable.Map(), + // Message id of the current group (no corresponding console.groupEnd yet). + currentGroup: null, +}); + +function messages(state = new MessageState(), action) { + const { + messagesById, + messagesUiById, + messagesTableDataById, + groupsById, + currentGroup + } = state; + + switch (action.type) { + case constants.MESSAGE_ADD: + let newMessage = action.message; + + if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) { + // When the message has a NULL type, we don't add it. + return state; + } + + if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) { + // Compute the new current group. + return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById)); + } + + if (newMessage.allowRepeating && messagesById.size > 0) { + let lastMessage = messagesById.last(); + if (lastMessage.repeatId === newMessage.repeatId) { + return state.withMutations(function (record) { + record.set("messagesById", messagesById.pop().push( + newMessage.set("repeat", lastMessage.repeat + 1) + )); + }); + } + } + + return state.withMutations(function (record) { + // Add the new message with a reference to the parent group. + record.set( + "messagesById", + messagesById.push(newMessage.set("groupId", currentGroup)) + ); + + if (newMessage.type === "trace") { + // We want the stacktrace to be open by default. + record.set("messagesUiById", messagesUiById.push(newMessage.id)); + } else if (isGroupType(newMessage.type)) { + record.set("currentGroup", newMessage.id); + record.set("groupsById", + groupsById.set( + newMessage.id, + getParentGroups(currentGroup, groupsById) + ) + ); + + if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) { + // We want the group to be open by default. + record.set("messagesUiById", messagesUiById.push(newMessage.id)); + } + } + }); + case constants.MESSAGES_CLEAR: + return state.withMutations(function (record) { + record.set("messagesById", Immutable.List()); + record.set("messagesUiById", Immutable.List()); + record.set("groupsById", Immutable.Map()); + record.set("currentGroup", null); + }); + case constants.MESSAGE_OPEN: + return state.set("messagesUiById", messagesUiById.push(action.id)); + case constants.MESSAGE_CLOSE: + let index = state.messagesUiById.indexOf(action.id); + return state.deleteIn(["messagesUiById", index]); + case constants.MESSAGE_TABLE_RECEIVE: + const {id, data} = action; + return state.set("messagesTableDataById", messagesTableDataById.set(id, data)); + } + + return state; +} + +function getNewCurrentGroup(currentGoup, groupsById) { + let newCurrentGroup = null; + if (currentGoup) { + // Retrieve the parent groups of the current group. + let parents = groupsById.get(currentGoup); + if (Array.isArray(parents) && parents.length > 0) { + // If there's at least one parent, make the first one the new currentGroup. + newCurrentGroup = parents[0]; + } + } + return newCurrentGroup; +} + +function getParentGroups(currentGroup, groupsById) { + let groups = []; + if (currentGroup) { + // If there is a current group, we add it as a parent + groups = [currentGroup]; + + // As well as all its parents, if it has some. + let parentGroups = groupsById.get(currentGroup); + if (Array.isArray(parentGroups) && parentGroups.length > 0) { + groups = groups.concat(parentGroups); + } + } + + return groups; +} + +exports.messages = messages; diff --git a/devtools/client/webconsole/new-console-output/reducers/moz.build b/devtools/client/webconsole/new-console-output/reducers/moz.build new file mode 100644 index 0000000000..651512f850 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'filters.js', + 'index.js', + 'messages.js', + 'prefs.js', + 'ui.js', +) diff --git a/devtools/client/webconsole/new-console-output/reducers/prefs.js b/devtools/client/webconsole/new-console-output/reducers/prefs.js new file mode 100644 index 0000000000..0707105e17 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/prefs.js @@ -0,0 +1,18 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const Immutable = require("devtools/client/shared/vendor/immutable"); +const PrefState = Immutable.Record({ + logLimit: 1000 +}); + +function prefs(state = new PrefState(), action) { + return state; +} + +exports.PrefState = PrefState; +exports.prefs = prefs; diff --git a/devtools/client/webconsole/new-console-output/reducers/ui.js b/devtools/client/webconsole/new-console-output/reducers/ui.js new file mode 100644 index 0000000000..aa91dceebe --- /dev/null +++ b/devtools/client/webconsole/new-console-output/reducers/ui.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const { + FILTER_BAR_TOGGLE, + MESSAGE_ADD, +} = require("devtools/client/webconsole/new-console-output/constants"); +const Immutable = require("devtools/client/shared/vendor/immutable"); + +const UiState = Immutable.Record({ + filterBarVisible: false, + filteredMessageVisible: false, + autoscroll: true, +}); + +function ui(state = new UiState(), action) { + // Autoscroll should be set for all action types. If the last action was not message + // add, then turn it off. This prevents us from scrolling after someone toggles a + // filter, or to the bottom of the attachement when an expandable message at the bottom + // of the list is expanded. It does depend on the MESSAGE_ADD action being the last in + // its batch, though. + state = state.set("autoscroll", action.type == MESSAGE_ADD); + + switch (action.type) { + case FILTER_BAR_TOGGLE: + return state.set("filterBarVisible", !state.filterBarVisible); + } + + return state; +} + +module.exports = { + UiState, + ui, +}; diff --git a/devtools/client/webconsole/new-console-output/selectors/filters.js b/devtools/client/webconsole/new-console-output/selectors/filters.js new file mode 100644 index 0000000000..36afa60cc4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/selectors/filters.js @@ -0,0 +1,12 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +function getAllFilters(state) { + return state.filters; +} + +exports.getAllFilters = getAllFilters; diff --git a/devtools/client/webconsole/new-console-output/selectors/messages.js b/devtools/client/webconsole/new-console-output/selectors/messages.js new file mode 100644 index 0000000000..c4b1aee282 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/selectors/messages.js @@ -0,0 +1,168 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); +const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters"); +const { getLogLimit } = require("devtools/client/webconsole/new-console-output/selectors/prefs"); +const { + MESSAGE_TYPE, + MESSAGE_SOURCE +} = require("devtools/client/webconsole/new-console-output/constants"); + +function getAllMessages(state) { + let messages = getAllMessagesById(state); + let logLimit = getLogLimit(state); + let filters = getAllFilters(state); + + let groups = getAllGroupsById(state); + let messagesUI = getAllMessagesUiById(state); + + return prune( + messages.filter(message => { + return ( + isInOpenedGroup(message, groups, messagesUI) + && ( + isUnfilterable(message) + || ( + matchLevelFilters(message, filters) + && matchNetworkFilters(message, filters) + && matchSearchFilters(message, filters) + ) + ) + ); + }), + logLimit + ); +} + +function getAllMessagesById(state) { + return state.messages.messagesById; +} + +function getAllMessagesUiById(state) { + return state.messages.messagesUiById; +} + +function getAllMessagesTableDataById(state) { + return state.messages.messagesTableDataById; +} + +function getAllGroupsById(state) { + return state.messages.groupsById; +} + +function getCurrentGroup(state) { + return state.messages.currentGroup; +} + +function isUnfilterable(message) { + return [ + MESSAGE_TYPE.COMMAND, + MESSAGE_TYPE.RESULT, + MESSAGE_TYPE.START_GROUP, + MESSAGE_TYPE.START_GROUP_COLLAPSED, + ].includes(message.type); +} + +function isInOpenedGroup(message, groups, messagesUI) { + return !message.groupId + || ( + !isGroupClosed(message.groupId, messagesUI) + && !hasClosedParentGroup(groups.get(message.groupId), messagesUI) + ); +} + +function hasClosedParentGroup(group, messagesUI) { + return group.some(groupId => isGroupClosed(groupId, messagesUI)); +} + +function isGroupClosed(groupId, messagesUI) { + return messagesUI.includes(groupId) === false; +} + +function matchLevelFilters(message, filters) { + return filters.get(message.level) === true; +} + +function matchNetworkFilters(message, filters) { + return ( + message.source !== MESSAGE_SOURCE.NETWORK + || (filters.get("net") === true && message.isXHR === false) + || (filters.get("netxhr") === true && message.isXHR === true) + ); +} + +function matchSearchFilters(message, filters) { + let text = filters.text || ""; + return ( + text === "" + // @TODO currently we return true for any object grip. We should find a way to + // search object grips. + || (message.parameters !== null && !Array.isArray(message.parameters)) + // Look for a match in location. + || isTextInFrame(text, message.frame) + // Look for a match in stacktrace. + || ( + Array.isArray(message.stacktrace) && + message.stacktrace.some(frame => isTextInFrame(text, + // isTextInFrame expect the properties of the frame object to be in the same + // order they are rendered in the Frame component. + { + functionName: frame.functionName || + l10n.getStr("stacktrace.anonymousFunction"), + filename: frame.filename, + lineNumber: frame.lineNumber, + columnNumber: frame.columnNumber + })) + ) + // Look for a match in messageText. + || (message.messageText !== null + && message.messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase())) + // Look for a match in parameters. Currently only checks value grips. + || (message.parameters !== null + && message.parameters.join("").toLocaleLowerCase() + .includes(text.toLocaleLowerCase())) + ); +} + +function isTextInFrame(text, frame) { + if (!frame) { + return false; + } + // @TODO Change this to Object.values once it's supported in Node's version of V8 + return Object.keys(frame) + .map(key => frame[key]) + .join(":") + .toLocaleLowerCase() + .includes(text.toLocaleLowerCase()); +} + +function prune(messages, logLimit) { + let messageCount = messages.count(); + if (messageCount > logLimit) { + // If the second non-pruned message is in a group, + // we want to return the group as the first non-pruned message. + let firstIndex = messages.size - logLimit; + let groupId = messages.get(firstIndex + 1).groupId; + + if (groupId) { + return messages.splice(0, firstIndex + 1) + .unshift( + messages.findLast((message) => message.id === groupId) + ); + } + return messages.splice(0, firstIndex); + } + + return messages; +} + +exports.getAllMessages = getAllMessages; +exports.getAllMessagesUiById = getAllMessagesUiById; +exports.getAllMessagesTableDataById = getAllMessagesTableDataById; +exports.getAllGroupsById = getAllGroupsById; +exports.getCurrentGroup = getCurrentGroup; diff --git a/devtools/client/webconsole/new-console-output/selectors/moz.build b/devtools/client/webconsole/new-console-output/selectors/moz.build new file mode 100644 index 0000000000..547f53542a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/selectors/moz.build @@ -0,0 +1,11 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'filters.js', + 'messages.js', + 'prefs.js', + 'ui.js', +) diff --git a/devtools/client/webconsole/new-console-output/selectors/prefs.js b/devtools/client/webconsole/new-console-output/selectors/prefs.js new file mode 100644 index 0000000000..18d8b678c2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/selectors/prefs.js @@ -0,0 +1,12 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +function getLogLimit(state) { + return state.prefs.logLimit; +} + +exports.getLogLimit = getLogLimit; diff --git a/devtools/client/webconsole/new-console-output/selectors/ui.js b/devtools/client/webconsole/new-console-output/selectors/ui.js new file mode 100644 index 0000000000..c9729e92d4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/selectors/ui.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +function getAllUi(state) { + return state.ui; +} + +function getScrollSetting(state) { + return getAllUi(state).autoscroll; +} + +module.exports = { + getAllUi, + getScrollSetting, +}; diff --git a/devtools/client/webconsole/new-console-output/store.js b/devtools/client/webconsole/new-console-output/store.js new file mode 100644 index 0000000000..8ad7947e9b --- /dev/null +++ b/devtools/client/webconsole/new-console-output/store.js @@ -0,0 +1,74 @@ +/* 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 {FilterState} = require("devtools/client/webconsole/new-console-output/reducers/filters"); +const {PrefState} = require("devtools/client/webconsole/new-console-output/reducers/prefs"); +const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui"); +const { + applyMiddleware, + combineReducers, + compose, + createStore +} = require("devtools/client/shared/vendor/redux"); +const { thunk } = require("devtools/client/shared/redux/middleware/thunk"); +const { + BATCH_ACTIONS, + PREFS, +} = require("devtools/client/webconsole/new-console-output/constants"); +const { reducers } = require("./reducers/index"); +const Services = require("Services"); + +function configureStore() { + const initialState = { + prefs: new PrefState({ + logLimit: Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1), + }), + filters: new FilterState({ + error: Services.prefs.getBoolPref(PREFS.FILTER.ERROR), + warn: Services.prefs.getBoolPref(PREFS.FILTER.WARN), + info: Services.prefs.getBoolPref(PREFS.FILTER.INFO), + log: Services.prefs.getBoolPref(PREFS.FILTER.LOG), + net: Services.prefs.getBoolPref(PREFS.FILTER.NET), + netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR), + }), + ui: new UiState({ + filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR), + }) + }; + + return createStore( + combineReducers(reducers), + initialState, + compose(applyMiddleware(thunk), enableBatching()) + ); +} + +/** + * A enhancer for the store to handle batched actions. + */ +function enableBatching() { + return next => (reducer, initialState, enhancer) => { + function batchingReducer(state, action) { + switch (action.type) { + case BATCH_ACTIONS: + return action.actions.reduce(batchingReducer, state); + default: + return reducer(state, action); + } + } + + if (typeof initialState === "function" && typeof enhancer === "undefined") { + enhancer = initialState; + initialState = undefined; + } + + return next(batchingReducer, initialState, enhancer); + }; +} + +// Provide the store factory for test code so that each test is working with +// its own instance. +module.exports.configureStore = configureStore; + diff --git a/devtools/client/webconsole/new-console-output/test/.eslintrc.js b/devtools/client/webconsole/new-console-output/test/.eslintrc.js new file mode 100644 index 0000000000..e010df3864 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + "extends": ["../../../../.eslintrc.xpcshell.js"] +}; diff --git a/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini new file mode 100644 index 0000000000..0543ae5c6d --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] + +support-files = + head.js + +[test_render_perf.html] +skip-if = true # Bug 1306783 diff --git a/devtools/client/webconsole/new-console-output/test/chrome/head.js b/devtools/client/webconsole/new-console-output/test/chrome/head.js new file mode 100644 index 0000000000..e8a5fd22e2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/chrome/head.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { utils: Cu } = Components; + +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var { Assert } = require("resource://testing-common/Assert.jsm"); +var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); +var { Task } = require("devtools/shared/task"); + +var { require: browserRequire } = BrowserLoader({ + baseURI: "resource://devtools/client/webconsole/", + window +}); diff --git a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html new file mode 100644 index 0000000000..d22819a2be --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for getRepeatId()</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.8" src="head.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for render perf</p> +<div id="output"></div> + +<script type="text/javascript;version=1.8"> +const testPackets = []; +const numMessages = 1000; +for (let id = 0; id < numMessages; id++) { + let message = "Odd text"; + if (id % 2 === 0) { + message = "Even text"; + } + testPackets.push({ + "from": "server1.conn4.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "foobar", + message, + id + ], + "columnNumber": 1, + "counter": null, + "filename": "file:///test.html", + "functionName": "", + "groupName": "", + "level": "log", + "lineNumber": 1, + "private": false, + "styles": [], + "timeStamp": 1455064271115 + id, + "timer": null, + "workerType": "none", + "category": "webdev" + } + }); +} + +function timeit(cb) { + // Return a Promise that resolves the number of seconds cb takes. + return new Promise(resolve => { + let start = performance.now(); + cb(); + let elapsed = performance.now() - start; + resolve(elapsed / 1000); + }); +} + +window.onload = Task.async(function* () { + const { configureStore } = browserRequire("devtools/client/webconsole/new-console-output/store"); + const { filterTextSet, filtersClear } = browserRequire("devtools/client/webconsole/new-console-output/actions/index"); + const NewConsoleOutputWrapper = browserRequire("devtools/client/webconsole/new-console-output/new-console-output-wrapper"); + const wrapper = new NewConsoleOutputWrapper(document.querySelector("#output"), {}); + + const store = configureStore(); + + let time = yield timeit(() => { + testPackets.forEach((message) => { + wrapper.dispatchMessageAdd(message); + }); + }); + info("took " + time + " seconds to render messages"); + + time = yield timeit(() => { + store.dispatch(filterTextSet("Odd text")); + }); + info("took " + time + " seconds to search filter half the messages"); + + time = yield timeit(() => { + store.dispatch(filtersClear()); + }); + info("took " + time + " seconds to clear the filter"); + + ok(true, "Yay, it didn't time out!"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js new file mode 100644 index 0000000000..3b4e2b196a --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test utils. +const expect = require("expect"); +const { render, mount } = require("enzyme"); +const sinon = require("sinon"); + +// React +const { createFactory } = require("devtools/client/shared/vendor/react"); +const Provider = createFactory(require("react-redux").Provider); +const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers"); + +// Components under test. +const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call")); +const { + MESSAGE_OPEN, + MESSAGE_CLOSE, +} = require("devtools/client/webconsole/new-console-output/constants"); +const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent"); + +// Test fakes. +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +const tempfilePath = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js"; + +describe("ConsoleAPICall component:", () => { + describe("console.log", () => { + it("renders string grips", () => { + const message = stubPreparedMessages.get("console.log('foobar', 'test')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe("foobar test"); + expect(wrapper.find(".objectBox-string").length).toBe(2); + expect(wrapper.find("div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace").length).toBe(1); + + // There should be the location + const locationLink = wrapper.find(`.message-location`); + expect(locationLink.length).toBe(1); + expect(locationLink.text()).toBe("test-tempfile.js:1:27"); + }); + + it("renders string grips with custom style", () => { + const message = stubPreparedMessages.get("console.log(%cfoobar)"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + const elements = wrapper.find(".objectBox-string"); + expect(elements.text()).toBe("foobar"); + expect(elements.length).toBe(2); + + const firstElementStyle = elements.eq(0).prop("style"); + // Allowed styles are applied accordingly on the first element. + expect(firstElementStyle.color).toBe(`blue`); + expect(firstElementStyle["font-size"]).toBe(`1.3em`); + // Forbidden styles are not applied. + expect(firstElementStyle["background-image"]).toBe(undefined); + expect(firstElementStyle.position).toBe(undefined); + expect(firstElementStyle.top).toBe(undefined); + + const secondElementStyle = elements.eq(1).prop("style"); + // Allowed styles are applied accordingly on the second element. + expect(secondElementStyle.color).toBe(`red`); + // Forbidden styles are not applied. + expect(secondElementStyle.background).toBe(undefined); + }); + + it("renders repeat node", () => { + const message = + stubPreparedMessages.get("console.log('foobar', 'test')") + .set("repeat", 107); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-repeats").text()).toBe("107"); + expect(wrapper.find(".message-repeats").prop("title")).toBe("107 repeats"); + + expect(wrapper.find("span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats").length).toBe(1); + }); + + it("has the expected indent", () => { + const message = stubPreparedMessages.get("console.log('foobar', 'test')"); + + const indent = 10; + let wrapper = render(ConsoleApiCall({ message, serviceContainer, indent })); + expect(wrapper.find(".indent").prop("style").width) + .toBe(`${indent * INDENT_WIDTH}px`); + + wrapper = render(ConsoleApiCall({ message, serviceContainer})); + expect(wrapper.find(".indent").prop("style").width).toBe(`0`); + }); + }); + + describe("console.count", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.count('bar')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe("bar: 1"); + }); + }); + + describe("console.assert", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.assert(false, {message: 'foobar'})"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe("Assertion failed: Object { message: \"foobar\" }"); + }); + }); + + describe("console.time", () => { + it("does not show anything", () => { + const message = stubPreparedMessages.get("console.time('bar')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe(""); + }); + }); + + describe("console.timeEnd", () => { + it("renders as expected", () => { + const message = stubPreparedMessages.get("console.timeEnd('bar')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe(message.messageText); + expect(wrapper.find(".message-body").text()).toMatch(/^bar: \d+(\.\d+)?ms$/); + }); + }); + + describe("console.trace", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.trace()"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true })); + const filepath = `${tempfilePath}`; + + expect(wrapper.find(".message-body").text()).toBe("console.trace()"); + + const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url='${filepath}']`); + expect(frameLinks.length).toBe(3); + + expect(frameLinks.eq(0).find(".frame-link-function-display-name").text()).toBe("testStacktraceFiltering"); + expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(filepath); + + expect(frameLinks.eq(1).find(".frame-link-function-display-name").text()).toBe("foo"); + expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(filepath); + + expect(frameLinks.eq(2).find(".frame-link-function-display-name").text()).toBe("triggerPacket"); + expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(filepath); + + //it should not be collapsible. + expect(wrapper.find(`.theme-twisty`).length).toBe(0); + }); + }); + + describe("console.group", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.group('bar')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true })); + + expect(wrapper.find(".message-body").text()).toBe(message.messageText); + expect(wrapper.find(".theme-twisty.open").length).toBe(1); + }); + + it("toggle the group when the collapse button is clicked", () => { + const store = setupStore([]); + store.dispatch = sinon.spy(); + const message = stubPreparedMessages.get("console.group('bar')"); + + let wrapper = mount(Provider({store}, + ConsoleApiCall({ + message, + open: true, + dispatch: store.dispatch, + serviceContainer, + }) + )); + wrapper.find(".theme-twisty.open").simulate("click"); + let call = store.dispatch.getCall(0); + expect(call.args[0]).toEqual({ + id: message.id, + type: MESSAGE_CLOSE + }); + + wrapper = mount(Provider({store}, + ConsoleApiCall({ + message, + open: false, + dispatch: store.dispatch, + serviceContainer, + }) + )); + wrapper.find(".theme-twisty").simulate("click"); + call = store.dispatch.getCall(1); + expect(call.args[0]).toEqual({ + id: message.id, + type: MESSAGE_OPEN + }); + }); + }); + + describe("console.groupEnd", () => { + it("does not show anything", () => { + const message = stubPreparedMessages.get("console.groupEnd('bar')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()).toBe(""); + }); + }); + + describe("console.groupCollapsed", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.groupCollapsed('foo')"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: false})); + + expect(wrapper.find(".message-body").text()).toBe(message.messageText); + expect(wrapper.find(".theme-twisty:not(.open)").length).toBe(1); + }); + }); + + describe("console.dirxml", () => { + it("renders", () => { + const message = stubPreparedMessages.get("console.dirxml(window)"); + const wrapper = render(ConsoleApiCall({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()) + .toBe("Window http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"); + }); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js new file mode 100644 index 0000000000..4d78908071 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test utils. +const expect = require("expect"); +const { render, mount } = require("enzyme"); +const sinon = require("sinon"); + +// React +const { createFactory } = require("devtools/client/shared/vendor/react"); +const Provider = createFactory(require("react-redux").Provider); +const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers"); + +// Components under test. +const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result")); +const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent"); + +// Test fakes. +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +describe("EvaluationResult component:", () => { + it("renders a grip result", () => { + const message = stubPreparedMessages.get("new Date(0)"); + const wrapper = render(EvaluationResult({ message })); + + expect(wrapper.find(".message-body").text()).toBe("Date 1970-01-01T00:00:00.000Z"); + + expect(wrapper.find(".message.log").length).toBe(1); + }); + + it("renders an error", () => { + const message = stubPreparedMessages.get("asdf()"); + const wrapper = render(EvaluationResult({ message })); + + expect(wrapper.find(".message-body").text()) + .toBe("ReferenceError: asdf is not defined[Learn More]"); + + expect(wrapper.find(".message.error").length).toBe(1); + }); + + it("displays a [Learn more] link", () => { + const store = setupStore([]); + + const message = stubPreparedMessages.get("asdf()"); + + serviceContainer.openLink = sinon.spy(); + const wrapper = mount(Provider({store}, + EvaluationResult({message, serviceContainer}) + )); + + const url = + "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined"; + const learnMore = wrapper.find(".learn-more-link"); + expect(learnMore.length).toBe(1); + expect(learnMore.prop("title")).toBe(url); + + learnMore.simulate("click"); + let call = serviceContainer.openLink.getCall(0); + expect(call.args[0]).toEqual(message.exceptionDocURL); + }); + + it("has the expected indent", () => { + const message = stubPreparedMessages.get("new Date(0)"); + + const indent = 10; + let wrapper = render(EvaluationResult({ message, indent})); + expect(wrapper.find(".indent").prop("style").width) + .toBe(`${indent * INDENT_WIDTH}px`); + + wrapper = render(EvaluationResult({ message})); + expect(wrapper.find(".indent").prop("style").width).toBe(`0`); + }); + + it("has location information", () => { + const message = stubPreparedMessages.get("1 + @"); + const wrapper = render(EvaluationResult({ message })); + + const locationLink = wrapper.find(`.message-location`); + expect(locationLink.length).toBe(1); + expect(locationLink.text()).toBe("debugger eval code:1:4"); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js new file mode 100644 index 0000000000..23f958cd94 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const expect = require("expect"); +const sinon = require("sinon"); +const { render, mount } = require("enzyme"); + +const { createFactory } = require("devtools/client/shared/vendor/react"); +const Provider = createFactory(require("react-redux").Provider); + +const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button")); +const FilterBar = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar")); +const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui"); +const { + MESSAGES_CLEAR, + MESSAGE_LEVEL +} = require("devtools/client/webconsole/new-console-output/constants"); + +const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +describe("FilterBar component:", () => { + it("initial render", () => { + const store = setupStore([]); + + const wrapper = render(Provider({store}, FilterBar({ serviceContainer }))); + const toolbar = wrapper.find( + ".devtools-toolbar.webconsole-filterbar-primary" + ); + + // Clear button + expect(toolbar.children().eq(0).attr("class")) + .toBe("devtools-button devtools-clear-icon"); + expect(toolbar.children().eq(0).attr("title")).toBe("Clear output"); + + // Filter bar toggle + expect(toolbar.children().eq(1).attr("class")) + .toBe("devtools-button devtools-filter-icon"); + expect(toolbar.children().eq(1).attr("title")).toBe("Toggle filter bar"); + + // Text filter + expect(toolbar.children().eq(2).attr("class")).toBe("devtools-plaininput text-filter"); + expect(toolbar.children().eq(2).attr("placeholder")).toBe("Filter output"); + expect(toolbar.children().eq(2).attr("type")).toBe("search"); + expect(toolbar.children().eq(2).attr("value")).toBe(""); + }); + + it("displays filter bar when button is clicked", () => { + const store = setupStore([]); + + expect(getAllUi(store.getState()).filterBarVisible).toBe(false); + + const wrapper = mount(Provider({store}, FilterBar({ serviceContainer }))); + wrapper.find(".devtools-filter-icon").simulate("click"); + + expect(getAllUi(store.getState()).filterBarVisible).toBe(true); + + // Buttons are displayed + const buttonProps = { + active: true, + dispatch: store.dispatch + }; + const logButton = FilterButton(Object.assign({}, buttonProps, + { label: "Logs", filterKey: MESSAGE_LEVEL.LOG })); + const debugButton = FilterButton(Object.assign({}, buttonProps, + { label: "Debug", filterKey: MESSAGE_LEVEL.DEBUG })); + const infoButton = FilterButton(Object.assign({}, buttonProps, + { label: "Info", filterKey: MESSAGE_LEVEL.INFO })); + const warnButton = FilterButton(Object.assign({}, buttonProps, + { label: "Warnings", filterKey: MESSAGE_LEVEL.WARN })); + const errorButton = FilterButton(Object.assign({}, buttonProps, + { label: "Errors", filterKey: MESSAGE_LEVEL.ERROR })); + expect(wrapper.contains([errorButton, warnButton, logButton, infoButton, debugButton])).toBe(true); + }); + + it("fires MESSAGES_CLEAR action when clear button is clicked", () => { + const store = setupStore([]); + store.dispatch = sinon.spy(); + + const wrapper = mount(Provider({store}, FilterBar({ serviceContainer }))); + wrapper.find(".devtools-clear-icon").simulate("click"); + const call = store.dispatch.getCall(0); + expect(call.args[0]).toEqual({ + type: MESSAGES_CLEAR + }); + }); + + it("sets filter text when text is typed", () => { + const store = setupStore([]); + + const wrapper = mount(Provider({store}, FilterBar({ serviceContainer }))); + wrapper.find(".devtools-plaininput").simulate("input", { target: { value: "a" } }); + expect(store.getState().filters.text).toBe("a"); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js new file mode 100644 index 0000000000..3774da0b83 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const expect = require("expect"); +const { render } = require("enzyme"); + +const { createFactory } = require("devtools/client/shared/vendor/react"); + +const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button")); +const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants"); + +describe("FilterButton component:", () => { + const props = { + active: true, + label: "Error", + filterKey: MESSAGE_LEVEL.ERROR, + }; + + it("displays as active when turned on", () => { + const wrapper = render(FilterButton(props)); + expect(wrapper.html()).toBe( + "<button class=\"menu-filter-button error checked\">Error</button>" + ); + }); + + it("displays as inactive when turned off", () => { + const inactiveProps = Object.assign({}, props, { active: false }); + const wrapper = render(FilterButton(inactiveProps)); + expect(wrapper.html()).toBe( + "<button class=\"menu-filter-button error\">Error</button>" + ); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/message-container.test.js b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js new file mode 100644 index 0000000000..2377af9065 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test utils. +const expect = require("expect"); +const { + renderComponent, + shallowRenderComponent +} = require("devtools/client/webconsole/new-console-output/test/helpers"); + +// Components under test. +const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container"); +const ConsoleApiCall = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call"); +const EvaluationResult = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result"); +const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error"); + +// Test fakes. +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +describe("MessageContainer component:", () => { + it("pipes data to children as expected", () => { + const message = stubPreparedMessages.get("console.log('foobar', 'test')"); + const rendered = renderComponent(MessageContainer, {message, serviceContainer}); + + expect(rendered.textContent.includes("foobar")).toBe(true); + }); + it("picks correct child component", () => { + const messageTypes = [ + { + component: ConsoleApiCall, + message: stubPreparedMessages.get("console.log('foobar', 'test')") + }, + { + component: EvaluationResult, + message: stubPreparedMessages.get("new Date(0)") + }, + { + component: PageError, + message: stubPreparedMessages.get("ReferenceError: asdf is not defined") + } + ]; + + messageTypes.forEach(info => { + const { component, message } = info; + const rendered = shallowRenderComponent(MessageContainer, { + message, + serviceContainer, + }); + expect(rendered.type).toBe(component); + }); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js new file mode 100644 index 0000000000..0244f08cf9 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { + MESSAGE_LEVEL, +} = require("devtools/client/webconsole/new-console-output/constants"); +const MessageIcon = require("devtools/client/webconsole/new-console-output/components/message-icon"); + +const expect = require("expect"); + +const { + renderComponent +} = require("devtools/client/webconsole/new-console-output/test/helpers"); + +describe("MessageIcon component:", () => { + it("renders icon based on level", () => { + const rendered = renderComponent(MessageIcon, { level: MESSAGE_LEVEL.ERROR }); + + expect(rendered.classList.contains("icon")).toBe(true); + expect(rendered.getAttribute("title")).toBe("Error"); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js new file mode 100644 index 0000000000..0257a3aad2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const MessageRepeat = require("devtools/client/webconsole/new-console-output/components/message-repeat"); + +const expect = require("expect"); + +const { + renderComponent +} = require("devtools/client/webconsole/new-console-output/test/helpers"); + +describe("MessageRepeat component:", () => { + it("renders repeated value correctly", () => { + const rendered = renderComponent(MessageRepeat, { repeat: 99 }); + expect(rendered.classList.contains("message-repeats")).toBe(true); + expect(rendered.style.visibility).toBe("visible"); + expect(rendered.textContent).toBe("99"); + }); + + it("renders an un-repeated value correctly", () => { + const rendered = renderComponent(MessageRepeat, { repeat: 1 }); + expect(rendered.style.visibility).toBe("hidden"); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js new file mode 100644 index 0000000000..8d0c5307e1 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test utils. +const expect = require("expect"); +const { render } = require("enzyme"); + +// React +const { createFactory } = require("devtools/client/shared/vendor/react"); + +// Components under test. +const NetworkEventMessage = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/network-event-message")); +const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent"); + +// Test fakes. +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +const EXPECTED_URL = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html"; + +describe("NetworkEventMessage component:", () => { + describe("GET request", () => { + it("renders as expected", () => { + const message = stubPreparedMessages.get("GET request"); + const wrapper = render(NetworkEventMessage({ message, serviceContainer })); + + expect(wrapper.find(".message-body .method").text()).toBe("GET"); + expect(wrapper.find(".message-body .xhr").length).toBe(0); + expect(wrapper.find(".message-body .url").length).toBe(1); + expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL); + expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1); + }); + + it("has the expected indent", () => { + const message = stubPreparedMessages.get("GET request"); + + const indent = 10; + let wrapper = render(NetworkEventMessage({ message, serviceContainer, indent})); + expect(wrapper.find(".indent").prop("style").width) + .toBe(`${indent * INDENT_WIDTH}px`); + + wrapper = render(NetworkEventMessage({ message, serviceContainer })); + expect(wrapper.find(".indent").prop("style").width).toBe(`0`); + }); + }); + + describe("XHR GET request", () => { + it("renders as expected", () => { + const message = stubPreparedMessages.get("XHR GET request"); + const wrapper = render(NetworkEventMessage({ message, serviceContainer })); + + expect(wrapper.find(".message-body .method").text()).toBe("GET"); + expect(wrapper.find(".message-body .xhr").length).toBe(1); + expect(wrapper.find(".message-body .xhr").text()).toBe("XHR"); + expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL); + expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1); + }); + }); + + describe("XHR POST request", () => { + it("renders as expected", () => { + const message = stubPreparedMessages.get("XHR POST request"); + const wrapper = render(NetworkEventMessage({ message, serviceContainer })); + + expect(wrapper.find(".message-body .method").text()).toBe("POST"); + expect(wrapper.find(".message-body .xhr").length).toBe(1); + expect(wrapper.find(".message-body .xhr").text()).toBe("XHR"); + expect(wrapper.find(".message-body .url").length).toBe(1); + expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL); + expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1); + }); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/components/page-error.test.js b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js new file mode 100644 index 0000000000..93f3a9ea56 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test utils. +const expect = require("expect"); +const { render, mount } = require("enzyme"); +const sinon = require("sinon"); + +// React +const { createFactory } = require("devtools/client/shared/vendor/react"); +const Provider = createFactory(require("react-redux").Provider); +const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers"); + +// Components under test. +const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error"); +const { + MESSAGE_OPEN, + MESSAGE_CLOSE, +} = require("devtools/client/webconsole/new-console-output/constants"); +const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent"); + +// Test fakes. +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer"); + +describe("PageError component:", () => { + it("renders", () => { + const message = stubPreparedMessages.get("ReferenceError: asdf is not defined"); + const wrapper = render(PageError({ message, serviceContainer })); + + expect(wrapper.find(".message-body").text()) + .toBe("ReferenceError: asdf is not defined[Learn More]"); + + // The stacktrace should be closed by default. + const frameLinks = wrapper.find(`.stack-trace`); + expect(frameLinks.length).toBe(0); + + // There should be the location. + const locationLink = wrapper.find(`.message-location`); + expect(locationLink.length).toBe(1); + // @TODO Will likely change. See https://github.com/devtools-html/gecko-dev/issues/285 + expect(locationLink.text()).toBe("test-tempfile.js:3:5"); + }); + + it("displays a [Learn more] link", () => { + const store = setupStore([]); + + const message = stubPreparedMessages.get("ReferenceError: asdf is not defined"); + + serviceContainer.openLink = sinon.spy(); + const wrapper = mount(Provider({store}, + PageError({message, serviceContainer}) + )); + + // There should be a [Learn more] link. + const url = + "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined"; + const learnMore = wrapper.find(".learn-more-link"); + expect(learnMore.length).toBe(1); + expect(learnMore.prop("title")).toBe(url); + + learnMore.simulate("click"); + let call = serviceContainer.openLink.getCall(0); + expect(call.args[0]).toEqual(message.exceptionDocURL); + }); + + it("has a stacktrace which can be openned", () => { + const message = stubPreparedMessages.get("ReferenceError: asdf is not defined"); + const wrapper = render(PageError({ message, serviceContainer, open: true })); + + // There should be a collapse button. + expect(wrapper.find(".theme-twisty.open").length).toBe(1); + + // There should be three stacktrace items. + const frameLinks = wrapper.find(`.stack-trace span.frame-link`); + expect(frameLinks.length).toBe(3); + }); + + it("toggle the stacktrace when the collapse button is clicked", () => { + const store = setupStore([]); + store.dispatch = sinon.spy(); + const message = stubPreparedMessages.get("ReferenceError: asdf is not defined"); + + let wrapper = mount(Provider({store}, + PageError({ + message, + open: true, + dispatch: store.dispatch, + serviceContainer, + }) + )); + wrapper.find(".theme-twisty.open").simulate("click"); + let call = store.dispatch.getCall(0); + expect(call.args[0]).toEqual({ + id: message.id, + type: MESSAGE_CLOSE + }); + + wrapper = mount(Provider({store}, + PageError({ + message, + open: false, + dispatch: store.dispatch, + serviceContainer, + }) + )); + wrapper.find(".theme-twisty").simulate("click"); + call = store.dispatch.getCall(1); + expect(call.args[0]).toEqual({ + id: message.id, + type: MESSAGE_OPEN + }); + }); + + it("has the expected indent", () => { + const message = stubPreparedMessages.get("ReferenceError: asdf is not defined"); + const indent = 10; + let wrapper = render(PageError({ message, serviceContainer, indent})); + expect(wrapper.find(".indent").prop("style").width) + .toBe(`${indent * INDENT_WIDTH}px`); + + wrapper = render(PageError({ message, serviceContainer})); + expect(wrapper.find(".indent").prop("style").width).toBe(`0`); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js new file mode 100644 index 0000000000..bb34bb477f --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// @TODO Load the actual strings from webconsole.properties instead. +class L10n { + getStr(str) { + switch (str) { + case "level.error": + return "Error"; + case "consoleCleared": + return "Console was cleared."; + case "webConsoleXhrIndicator": + return "XHR"; + case "webConsoleMoreInfoLabel": + return "Learn More"; + } + return str; + } + + getFormatStr(str) { + return this.getStr(str); + } +} + +module.exports = L10n; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js new file mode 100644 index 0000000000..8e6e9428c6 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LocalizationHelper = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n"); + +module.exports = { + LocalizationHelper +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js new file mode 100644 index 0000000000..87a058d5c7 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +class ObjectClient { +} + +module.exports = ObjectClient; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js new file mode 100644 index 0000000000..9ab3ad3ec2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +module.exports = { + PluralForm: { + get: function (occurence, str) { + // @TODO Remove when loading the actual strings from webconsole.properties + // is done in the L10n fixture. + if (str === "messageRepeats.tooltip2") { + return `${occurence} repeats`; + } + + return str; + } + } +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/Services.js b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js new file mode 100644 index 0000000000..61b3d5e138 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PREFS } = require("devtools/client/webconsole/new-console-output/constants"); + +module.exports = { + prefs: { + getIntPref: pref => { + switch (pref) { + case "devtools.hud.loglimit": + return 1000; + } + }, + getBoolPref: pref => { + const falsey = [ + PREFS.FILTER.NET, + PREFS.FILTER.NETXHR, + PREFS.UI.FILTER_BAR, + ]; + return !falsey.includes(pref); + }, + setBoolPref: () => {}, + clearUserPref: () => {}, + } +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js new file mode 100644 index 0000000000..5ab1c0bb4d --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n"); + +const Utils = { + L10n +}; + +module.exports = { + Utils +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build new file mode 100644 index 0000000000..ff41d6c805 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build @@ -0,0 +1,9 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'stub-generators', + 'stubs' +] diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js new file mode 100644 index 0000000000..04b15c88bc --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +module.exports = { + attachRefToHud: () => {}, + emitNewMessage: () => {}, + hudProxyClient: {}, + onViewSourceInDebugger: () => {}, + openNetworkPanel: () => {}, + sourceMapService: { + subscribe: () => {}, + }, + openLink: () => {}, + createElement: tagName => document.createElement(tagName) +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini new file mode 100644 index 0000000000..9f348544fa --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini @@ -0,0 +1,18 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + !/devtools/client/framework/test/shared-head.js + test-console-api.html + test-network-event.html + test-tempfile.js + +[browser_webconsole_update_stubs_console_api.js] +skip-if=true # This is only used to update stubs. It is not an actual test. +[browser_webconsole_update_stubs_evaluation_result.js] +skip-if=true # This is only used to update stubs. It is not an actual test. +[browser_webconsole_update_stubs_network_event.js] +skip-if=true # This is only used to update stubs. It is not an actual test. +[browser_webconsole_update_stubs_page_error.js] +skip-if=true # This is only used to update stubs. It is not an actual test. diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js new file mode 100644 index 0000000000..fc859a0029 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; +requestLongerTimeout(2) + +Cu.import("resource://gre/modules/osfile.jsm"); +const { consoleApi: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js"); + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"; + +let stubs = { + preparedMessages: [], + packets: [], +}; + +add_task(function* () { + for (var [key, {keys, code}] of snippets) { + yield OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`); + + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let {ui} = toolbox.getCurrentPanel().hud; + + ok(ui.jsterm, "jsterm exists"); + ok(ui.newConsoleOutput, "newConsoleOutput exists"); + + let received = new Promise(resolve => { + let i = 0; + let listener = (type, res) => { + stubs.packets.push(formatPacket(keys[i], res)); + stubs.preparedMessages.push(formatStub(keys[i], res)); + if(++i === keys.length ){ + toolbox.target.client.removeListener("consoleAPICall", listener); + resolve(); + } + }; + toolbox.target.client.addListener("consoleAPICall", listener); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) { + var script = content.document.createElement("script"); + script.src = "test-tempfile.js?key=" + encodeURIComponent(key); + script.onload = function() { content.wrappedJSObject.triggerPacket(); } + content.document.body.appendChild(script); + }); + + yield received; + + yield closeTabAndToolbox(); + } + let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "consoleApi.js"); + OS.File.writeAtomic(filePath, formatFile(stubs)); + OS.File.writeAtomic(TEMP_FILE_PATH, ""); +}); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js new file mode 100644 index 0000000000..507201a244 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +Cu.import("resource://gre/modules/osfile.jsm"); +const TEST_URI = "data:text/html;charset=utf-8,stub generation"; + +const { evaluationResult: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js"); + +let stubs = { + preparedMessages: [], + packets: [], +}; + +add_task(function* () { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + ok(true, "make the test not fail"); + + for (var [code,key] of snippets) { + const packet = yield new Promise(resolve => { + toolbox.target.activeConsole.evaluateJS(code, resolve); + }); + stubs.packets.push(formatPacket(key, packet)); + stubs.preparedMessages.push(formatStub(key, packet)); + } + + let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "evaluationResult.js"); + OS.File.writeAtomic(filePath, formatFile(stubs)); +}); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js new file mode 100644 index 0000000000..cc018f634e --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +Cu.import("resource://gre/modules/osfile.jsm"); +const TARGET = "networkEvent"; +const { [TARGET]: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js"); +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html"; + +let stubs = { + preparedMessages: [], + packets: [], +}; + +add_task(function* () { + for (var [key, {keys, code}] of snippets) { + OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`); + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let {ui} = toolbox.getCurrentPanel().hud; + + ok(ui.jsterm, "jsterm exists"); + ok(ui.newConsoleOutput, "newConsoleOutput exists"); + + let received = new Promise(resolve => { + let i = 0; + toolbox.target.client.addListener(TARGET, (type, res) => { + stubs.packets.push(formatPacket(keys[i], res)); + stubs.preparedMessages.push(formatNetworkStub(keys[i], res)); + if(++i === keys.length ){ + resolve(); + } + }); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { + content.wrappedJSObject.triggerPacket(); + }); + + yield received; + } + let filePath = OS.Path.join(`${BASE_PATH}/stubs/${TARGET}.js`); + OS.File.writeAtomic(filePath, formatFile(stubs)); + OS.File.writeAtomic(TEMP_FILE_PATH, ""); +}); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js new file mode 100644 index 0000000000..9323e0031b --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +Cu.import("resource://gre/modules/osfile.jsm"); +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"; + +const { pageError: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js"); + +let stubs = { + preparedMessages: [], + packets: [], +}; + +add_task(function* () { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + ok(true, "make the test not fail"); + + for (var [key,code] of snippets) { + OS.File.writeAtomic(TEMP_FILE_PATH, `${code}`); + let received = new Promise(resolve => { + toolbox.target.client.addListener("pageError", function onPacket(e, packet) { + toolbox.target.client.removeListener("pageError", onPacket); + info("Received page error:" + e + " " + JSON.stringify(packet, null, "\t")); + + let message = prepareMessage(packet, {getNextId: () => 1}); + stubs.packets.push(formatPacket(message.messageText, packet)); + stubs.preparedMessages.push(formatStub(message.messageText, packet)); + resolve(); + }); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) { + var script = content.document.createElement("script"); + script.src = "test-tempfile.js?key=" + encodeURIComponent(key); + content.document.body.appendChild(script); + }); + + yield received; + } + + let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "pageError.js"); + OS.File.writeAtomic(filePath, formatFile(stubs)); + OS.File.writeAtomic(TEMP_FILE_PATH, ""); +}); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js new file mode 100644 index 0000000000..be988b9d8e --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js @@ -0,0 +1,192 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ +/* import-globals-from ../../../../framework/test/shared-head.js */ + +"use strict"; + +// shared-head.js handles imports, constants, and utility functions +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", + this); + +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); +}); + +const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages"); +const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js"); + +const BASE_PATH = "../../../../devtools/client/webconsole/new-console-output/test/fixtures"; +const TEMP_FILE_PATH = OS.Path.join(`${BASE_PATH}/stub-generators`, "test-tempfile.js"); + +let cachedPackets = {}; + +function getCleanedPacket(key, packet) { + if(Object.keys(cachedPackets).includes(key)) { + return cachedPackets[key]; + } + + // Strip escaped characters. + let safeKey = key + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\r") + .replace(/\\\"/g, `\"`) + .replace(/\\\'/g, `\'`); + + // If the stub already exist, we want to ignore irrelevant properties + // (actor, timeStamp, timer, ...) that might changed and "pollute" + // the diff resulting from this stub generation. + let res; + if(stubPackets.has(safeKey)) { + + let existingPacket = stubPackets.get(safeKey); + res = Object.assign({}, packet, { + from: existingPacket.from + }); + + // Clean root timestamp. + if(res.timestamp) { + res.timestamp = existingPacket.timestamp; + } + + if (res.message) { + // Clean timeStamp on the message prop. + res.message.timeStamp = existingPacket.message.timeStamp; + if (res.message.timer) { + // Clean timer properties on the message. + // Those properties are found on console.time and console.timeEnd calls, + // and those time can vary, which is why we need to clean them. + if (res.message.timer.started) { + res.message.timer.started = existingPacket.message.timer.started; + } + if (res.message.timer.duration) { + res.message.timer.duration = existingPacket.message.timer.duration; + } + } + + if(Array.isArray(res.message.arguments)) { + // Clean actor ids on each message.arguments item. + res.message.arguments.forEach((argument, i) => { + if (argument && argument.actor) { + argument.actor = existingPacket.message.arguments[i].actor; + } + }); + } + } + + if (res.result) { + // Clean actor ids on evaluation result messages. + res.result.actor = existingPacket.result.actor; + if (res.result.preview) { + if(res.result.preview.timestamp) { + // Clean timestamp there too. + res.result.preview.timestamp = existingPacket.result.preview.timestamp; + } + } + } + + if (res.exception) { + // Clean actor ids on exception messages. + res.exception.actor = existingPacket.exception.actor; + if (res.exception.preview) { + if(res.exception.preview.timestamp) { + // Clean timestamp there too. + res.exception.preview.timestamp = existingPacket.exception.preview.timestamp; + } + } + } + + if (res.eventActor) { + // Clean actor ids, timeStamp and startedDateTime on network messages. + res.eventActor.actor = existingPacket.eventActor.actor; + res.eventActor.startedDateTime = existingPacket.eventActor.startedDateTime; + res.eventActor.timeStamp = existingPacket.eventActor.timeStamp; + } + + if (res.pageError) { + // Clean timeStamp on pageError messages. + res.pageError.timeStamp = existingPacket.pageError.timeStamp; + } + + } else { + res = packet; + } + + cachedPackets[key] = res; + return res; +} + +function formatPacket(key, packet) { + return ` +stubPackets.set("${key}", ${JSON.stringify(getCleanedPacket(key, packet), null, "\t")}); +`; +} + +function formatStub(key, packet) { + let prepared = prepareMessage( + getCleanedPacket(key, packet), + {getNextId: () => "1"} + ); + + return ` +stubPreparedMessages.set("${key}", new ConsoleMessage(${JSON.stringify(prepared, null, "\t")})); +`; +} + +function formatNetworkStub(key, packet) { + let actor = packet.eventActor; + let networkInfo = { + _type: "NetworkEvent", + timeStamp: actor.timeStamp, + node: null, + actor: actor.actor, + discardRequestBody: true, + discardResponseBody: true, + startedDateTime: actor.startedDateTime, + request: { + url: actor.url, + method: actor.method, + }, + isXHR: actor.isXHR, + cause: actor.cause, + response: {}, + timings: {}, + // track the list of network event updates + updates: [], + private: actor.private, + fromCache: actor.fromCache, + fromServiceWorker: actor.fromServiceWorker + }; + let prepared = prepareMessage(networkInfo, {getNextId: () => "1"}); + return ` +stubPreparedMessages.set("${key}", new NetworkEventMessage(${JSON.stringify(prepared, null, "\t")})); +`; +} + +function formatFile(stubs) { + return `/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types"); + +let stubPreparedMessages = new Map(); +let stubPackets = new Map(); + +${stubs.preparedMessages.join("")} +${stubs.packets.join("")} + +module.exports = { + stubPreparedMessages, + stubPackets, +}`; +} diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build new file mode 100644 index 0000000000..4b4e8a1d82 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'stub-snippets.js', +) diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js new file mode 100644 index 0000000000..f79548e7bc --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {DebuggerServer} = require("devtools/server/main"); +var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a"); +var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + +// Console API + +const consoleApiCommands = [ + "console.log('foobar', 'test')", + "console.log(undefined)", + "console.warn('danger, will robinson!')", + "console.log(NaN)", + "console.log(null)", + "console.log('\u9f2c')", + "console.clear()", + "console.count('bar')", + "console.assert(false, {message: 'foobar'})", + "console.log('hello \\nfrom \\rthe \\\"string world!')", + "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')", + "console.dirxml(window)", +]; + +let consoleApi = new Map(consoleApiCommands.map( + cmd => [cmd, {keys: [cmd], code: cmd}])); + +consoleApi.set("console.trace()", { + keys: ["console.trace()"], + code: ` +function testStacktraceFiltering() { + console.trace() +} +function foo() { + testStacktraceFiltering() +} + +foo() +`}); + +consoleApi.set("console.time('bar')", { + keys: ["console.time('bar')", "console.timeEnd('bar')"], + code: ` +console.time("bar"); +console.timeEnd("bar"); +`}); + +consoleApi.set("console.table('bar')", { + keys: ["console.table('bar')"], + code: ` +console.table('bar'); +`}); + +consoleApi.set("console.table(['a', 'b', 'c'])", { + keys: ["console.table(['a', 'b', 'c'])"], + code: ` +console.table(['a', 'b', 'c']); +`}); + +consoleApi.set("console.group('bar')", { + keys: ["console.group('bar')", "console.groupEnd('bar')"], + code: ` +console.group("bar"); +console.groupEnd("bar"); +`}); + +consoleApi.set("console.groupCollapsed('foo')", { + keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"], + code: ` +console.groupCollapsed("foo"); +console.groupEnd("foo"); +`}); + +consoleApi.set("console.group()", { + keys: ["console.group()", "console.groupEnd()"], + code: ` +console.group(); +console.groupEnd(); +`}); + +consoleApi.set("console.log(%cfoobar)", { + keys: ["console.log(%cfoobar)"], + code: ` +console.log( + "%cfoo%cbar", + "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px", + "color:red;background:\\165rl('http://example.com/test')"); +`}); + +// Evaluation Result +const evaluationResultCommands = [ + "new Date(0)", + "asdf()", + "1 + @" +]; + +let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd])); + +// Network Event + +let networkEvent = new Map(); + +networkEvent.set("GET request", { + keys: ["GET request"], + code: ` +let i = document.createElement("img"); +i.src = "inexistent.html"; +`}); + +networkEvent.set("XHR GET request", { + keys: ["XHR GET request"], + code: ` +const xhr = new XMLHttpRequest(); +xhr.open("GET", "inexistent.html"); +xhr.send(); +`}); + +networkEvent.set("XHR POST request", { + keys: ["XHR POST request"], + code: ` +const xhr = new XMLHttpRequest(); +xhr.open("POST", "inexistent.html"); +xhr.send(); +`}); + +// Page Error + +let pageError = new Map(); + +pageError.set("Reference Error", ` + function bar() { + asdf() + } + function foo() { + bar() + } + + foo() +`); + +module.exports = { + consoleApi, + evaluationResult, + networkEvent, + pageError, +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html new file mode 100644 index 0000000000..3246cff154 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Stub generator</title> + </head> + <body> + <p>Stub generator</p> + <script src="test-tempfile.js"></script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html new file mode 100644 index 0000000000..c234acea62 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Stub generator for network event</title> + </head> + <body> + <p>Stub generator for network event</p> + <script src="test-tempfile.js"></script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js new file mode 100644 index 0000000000..26e95fe39f --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js @@ -0,0 +1,1482 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types"); + +let stubPreparedMessages = new Map(); +let stubPackets = new Map(); + + +stubPreparedMessages.set("console.log('foobar', 'test')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "foobar", + "test" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foobar\",\"test\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log(undefined)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + { + "type": "undefined" + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"undefined\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.warn('danger, will robinson!')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "warn", + "level": "warn", + "messageText": null, + "parameters": [ + "danger, will robinson!" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"warn\",\"level\":\"warn\",\"messageText\":null,\"parameters\":[\"danger, will robinson!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log(NaN)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + { + "type": "NaN" + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"NaN\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log(null)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + { + "type": "null" + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"null\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log('鼬')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "鼬" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"鼬\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.clear()", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "clear", + "level": "log", + "messageText": null, + "parameters": [ + "Console was cleared." + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"clear\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.count('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "debug", + "messageText": "bar: 1", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"debug\",\"messageText\":\"bar: 1\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.assert(false, {message: 'foobar'})", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "assert", + "level": "error", + "messageText": null, + "parameters": [ + { + "type": "object", + "actor": "server1.conn8.child1/obj31", + "class": "Object", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 1, + "preview": { + "kind": "Object", + "ownProperties": { + "message": { + "configurable": true, + "enumerable": true, + "writable": true, + "value": "foobar" + } + }, + "ownPropertiesLength": 1, + "safeGetterValues": {} + } + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"assert\",\"level\":\"error\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn8.child1/obj31\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"message\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"foobar\"}},\"ownPropertiesLength\":1,\"safeGetterValues\":{}}}],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":27,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":1}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": [ + { + "columnNumber": 27, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)", + "functionName": "triggerPacket", + "language": 2, + "lineNumber": 1 + } + ], + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log('hello \nfrom \rthe \"string world!')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "hello \nfrom \rthe \"string world!" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"hello \\nfrom \\rthe \\\"string world!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log('úṇĩçödê țĕșť')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "úṇĩçödê țĕșť" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"úṇĩçödê țĕșť\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.dirxml(window)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + { + "type": "object", + "actor": "server1.conn11.child1/obj31", + "class": "Window", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 804, + "preview": { + "kind": "ObjectWithURL", + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html" + } + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn11.child1/obj31\",\"class\":\"Window\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":804,\"preview\":{\"kind\":\"ObjectWithURL\",\"url\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\"}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)", + "line": 1, + "column": 27 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.trace()", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "trace", + "level": "log", + "messageText": null, + "parameters": [], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"trace\",\"level\":\"log\",\"messageText\":null,\"parameters\":[],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"testStacktraceFiltering\",\"language\":2,\"lineNumber\":3},{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"foo\",\"language\":2,\"lineNumber\":6},{\"columnNumber\":1,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":9}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"line\":3,\"column\":3},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": [ + { + "columnNumber": 3, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "testStacktraceFiltering", + "language": 2, + "lineNumber": 3 + }, + { + "columnNumber": 3, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "foo", + "language": 2, + "lineNumber": 6 + }, + { + "columnNumber": 1, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "triggerPacket", + "language": 2, + "lineNumber": 9 + } + ], + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "line": 3, + "column": 3 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.time('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "nullMessage", + "level": "log", + "messageText": null, + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"nullMessage\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.timeEnd('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "timeEnd", + "level": "log", + "messageText": "bar: 1.36ms", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.36ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)", + "line": 3, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.table('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "bar" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.table(['a', 'b', 'c'])", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "table", + "level": "log", + "messageText": null, + "parameters": [ + { + "type": "object", + "actor": "server1.conn15.child1/obj31", + "class": "Array", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 4, + "preview": { + "kind": "ArrayLike", + "length": 3, + "items": [ + "a", + "b", + "c" + ] + } + } + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn15.child1/obj31\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.group('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "startGroup", + "level": "log", + "messageText": "bar", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"bar\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.groupEnd('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "endGroup", + "level": "log", + "messageText": null, + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)", + "line": 3, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.groupCollapsed('foo')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "startGroupCollapsed", + "level": "log", + "messageText": "foo", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroupCollapsed\",\"level\":\"log\",\"messageText\":\"foo\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.groupEnd('foo')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "endGroup", + "level": "log", + "messageText": null, + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)", + "line": 3, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.group()", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "startGroup", + "level": "log", + "messageText": "<no group label>", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"<no group label>\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.groupEnd()", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "endGroup", + "level": "log", + "messageText": null, + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()", + "line": 3, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [] +})); + +stubPreparedMessages.set("console.log(%cfoobar)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "log", + "level": "log", + "messageText": null, + "parameters": [ + "foo", + "bar" + ], + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)", + "line": 2, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": null, + "userProvidedStyles": [ + "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px", + "color:red;background:url('http://example.com/test')" + ] +})); + + +stubPackets.set("console.log('foobar', 'test')", { + "from": "server1.conn0.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "foobar", + "test" + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086261590, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log(undefined)", { + "from": "server1.conn1.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "undefined" + } + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086264886, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.warn('danger, will robinson!')", { + "from": "server1.conn2.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "danger, will robinson!" + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "warn", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086267284, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log(NaN)", { + "from": "server1.conn3.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "NaN" + } + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086269484, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log(null)", { + "from": "server1.conn4.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "null" + } + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086271418, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log('鼬')", { + "from": "server1.conn5.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "鼬" + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086273549, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.clear()", { + "from": "server1.conn6.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()", + "functionName": "triggerPacket", + "groupName": "", + "level": "clear", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086275587, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.count('bar')", { + "from": "server1.conn7.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 27, + "counter": { + "count": 1, + "label": "bar" + }, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "count", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086277812, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.assert(false, {message: 'foobar'})", { + "from": "server1.conn8.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "object", + "actor": "server1.conn8.child1/obj31", + "class": "Object", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 1, + "preview": { + "kind": "Object", + "ownProperties": { + "message": { + "configurable": true, + "enumerable": true, + "writable": true, + "value": "foobar" + } + }, + "ownPropertiesLength": 1, + "safeGetterValues": {} + } + } + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)", + "functionName": "triggerPacket", + "groupName": "", + "level": "assert", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086280131, + "timer": null, + "stacktrace": [ + { + "columnNumber": 27, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)", + "functionName": "triggerPacket", + "language": 2, + "lineNumber": 1 + } + ], + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log('hello \nfrom \rthe \"string world!')", { + "from": "server1.conn9.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "hello \nfrom \rthe \"string world!" + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086281936, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.log('úṇĩçödê țĕșť')", { + "from": "server1.conn10.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "úṇĩçödê țĕșť" + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [], + "timeStamp": 1477086283713, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + +stubPackets.set("console.dirxml(window)", { + "from": "server1.conn11.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "object", + "actor": "server1.conn11.child1/obj31", + "class": "Window", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 804, + "preview": { + "kind": "ObjectWithURL", + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html" + } + } + ], + "columnNumber": 27, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)", + "functionName": "triggerPacket", + "groupName": "", + "level": "dirxml", + "lineNumber": 1, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086285483, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.trace()", { + "from": "server1.conn12.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [], + "columnNumber": 3, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "testStacktraceFiltering", + "groupName": "", + "level": "trace", + "lineNumber": 3, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086287286, + "timer": null, + "stacktrace": [ + { + "columnNumber": 3, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "testStacktraceFiltering", + "language": 2, + "lineNumber": 3 + }, + { + "columnNumber": 3, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "foo", + "language": 2, + "lineNumber": 6 + }, + { + "columnNumber": 1, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()", + "functionName": "triggerPacket", + "language": 2, + "lineNumber": 9 + } + ], + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.time('bar')", { + "from": "server1.conn13.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "time", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086289137, + "timer": { + "name": "bar", + "started": 1166.305 + }, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.timeEnd('bar')", { + "from": "server1.conn13.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "timeEnd", + "lineNumber": 3, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086289138, + "timer": { + "duration": 1.3550000000000182, + "name": "bar" + }, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.table('bar')", { + "from": "server1.conn14.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "", + "level": "table", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086290984, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.table(['a', 'b', 'c'])", { + "from": "server1.conn15.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + { + "type": "object", + "actor": "server1.conn15.child1/obj31", + "class": "Array", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 4, + "preview": { + "kind": "ArrayLike", + "length": 3, + "items": [ + "a", + "b", + "c" + ] + } + } + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)", + "functionName": "triggerPacket", + "groupName": "", + "level": "table", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086292762, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.group('bar')", { + "from": "server1.conn16.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "bar", + "level": "group", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086294628, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.groupEnd('bar')", { + "from": "server1.conn16.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)", + "functionName": "triggerPacket", + "groupName": "bar", + "level": "groupEnd", + "lineNumber": 3, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086294630, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.groupCollapsed('foo')", { + "from": "server1.conn17.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "foo" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)", + "functionName": "triggerPacket", + "groupName": "foo", + "level": "groupCollapsed", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086296567, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.groupEnd('foo')", { + "from": "server1.conn17.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "foo" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)", + "functionName": "triggerPacket", + "groupName": "foo", + "level": "groupEnd", + "lineNumber": 3, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086296570, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.group()", { + "from": "server1.conn18.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()", + "functionName": "triggerPacket", + "groupName": "", + "level": "group", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086298462, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.groupEnd()", { + "from": "server1.conn18.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()", + "functionName": "triggerPacket", + "groupName": "", + "level": "groupEnd", + "lineNumber": 3, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "timeStamp": 1477086298464, + "timer": null, + "workerType": "none", + "styles": [], + "category": "webdev" + } +}); + +stubPackets.set("console.log(%cfoobar)", { + "from": "server1.conn19.child1/consoleActor2", + "type": "consoleAPICall", + "message": { + "arguments": [ + "foo", + "bar" + ], + "columnNumber": 1, + "counter": null, + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)", + "functionName": "triggerPacket", + "groupName": "", + "level": "log", + "lineNumber": 2, + "originAttributes": { + "addonId": "", + "appId": 0, + "firstPartyDomain": "", + "inIsolatedMozBrowser": false, + "privateBrowsingId": 0, + "userContextId": 0 + }, + "private": false, + "styles": [ + "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px", + "color:red;background:url('http://example.com/test')" + ], + "timeStamp": 1477086300265, + "timer": null, + "workerType": "none", + "category": "webdev" + } +}); + + +module.exports = { + stubPreparedMessages, + stubPackets, +}
\ No newline at end of file diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js new file mode 100644 index 0000000000..098086044e --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types"); + +let stubPreparedMessages = new Map(); +let stubPackets = new Map(); + + +stubPreparedMessages.set("new Date(0)", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "javascript", + "type": "result", + "level": "log", + "parameters": { + "type": "object", + "actor": "server1.conn0.child1/obj30", + "class": "Date", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 0, + "preview": { + "timestamp": 0 + } + }, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null,\"groupId\":null,\"userProvidedStyles\":null}", + "stacktrace": null, + "frame": null, + "groupId": null, + "userProvidedStyles": null +})); + +stubPreparedMessages.set("asdf()", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "javascript", + "type": "result", + "level": "error", + "messageText": "ReferenceError: asdf is not defined", + "parameters": { + "type": "undefined" + }, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":1},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null}", + "stacktrace": null, + "frame": { + "source": "debugger eval code", + "line": 1, + "column": 1 + }, + "groupId": null, + "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default", + "userProvidedStyles": null +})); + +stubPreparedMessages.set("1 + @", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "javascript", + "type": "result", + "level": "error", + "messageText": "SyntaxError: illegal character", + "parameters": { + "type": "undefined" + }, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"userProvidedStyles\":null}", + "stacktrace": null, + "frame": { + "source": "debugger eval code", + "line": 1, + "column": 4 + }, + "groupId": null, + "userProvidedStyles": null +})); + + +stubPackets.set("new Date(0)", { + "from": "server1.conn0.child1/consoleActor2", + "input": "new Date(0)", + "result": { + "type": "object", + "actor": "server1.conn0.child1/obj30", + "class": "Date", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 0, + "preview": { + "timestamp": 0 + } + }, + "timestamp": 1476573073424, + "exception": null, + "frame": null, + "helperResult": null +}); + +stubPackets.set("asdf()", { + "from": "server1.conn0.child1/consoleActor2", + "input": "asdf()", + "result": { + "type": "undefined" + }, + "timestamp": 1476573073442, + "exception": { + "type": "object", + "actor": "server1.conn0.child1/obj32", + "class": "Error", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 4, + "preview": { + "kind": "Error", + "name": "ReferenceError", + "message": "asdf is not defined", + "stack": "@debugger eval code:1:1\n", + "fileName": "debugger eval code", + "lineNumber": 1, + "columnNumber": 1 + } + }, + "exceptionMessage": "ReferenceError: asdf is not defined", + "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default", + "frame": { + "source": "debugger eval code", + "line": 1, + "column": 1 + }, + "helperResult": null +}); + +stubPackets.set("1 + @", { + "from": "server1.conn0.child1/consoleActor2", + "input": "1 + @", + "result": { + "type": "undefined" + }, + "timestamp": 1478755616654, + "exception": { + "type": "object", + "actor": "server1.conn0.child1/obj33", + "class": "Error", + "extensible": true, + "frozen": false, + "sealed": false, + "ownPropertyLength": 4, + "preview": { + "kind": "Error", + "name": "SyntaxError", + "message": "illegal character", + "stack": "", + "fileName": "debugger eval code", + "lineNumber": 1, + "columnNumber": 4 + } + }, + "exceptionMessage": "SyntaxError: illegal character", + "frame": { + "source": "debugger eval code", + "line": 1, + "column": 4 + }, + "helperResult": null +}); + + +module.exports = { + stubPreparedMessages, + stubPackets, +}
\ No newline at end of file diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js new file mode 100644 index 0000000000..59b4201804 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let maps = []; + +[ + "consoleApi", + "evaluationResult", + "networkEvent", + "pageError", +].forEach((filename) => { + maps[filename] = require(`./${filename}`); +}); + +// Combine all the maps into a single map. +module.exports = { + stubPreparedMessages: new Map([ + ...maps.consoleApi.stubPreparedMessages, + ...maps.evaluationResult.stubPreparedMessages, + ...maps.networkEvent.stubPreparedMessages, + ...maps.pageError.stubPreparedMessages, ]), + stubPackets: new Map([ + ...maps.consoleApi.stubPackets, + ...maps.evaluationResult.stubPackets, + ...maps.networkEvent.stubPackets, + ...maps.pageError.stubPackets, ]), +}; diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build new file mode 100644 index 0000000000..88e9c46dfa --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build @@ -0,0 +1,11 @@ +# 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/. + +DevToolsModules( + 'consoleApi.js', + 'evaluationResult.js', + 'index.js', + 'networkEvent.js', + 'pageError.js', +) diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js new file mode 100644 index 0000000000..58a40d30b9 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types"); + +let stubPreparedMessages = new Map(); +let stubPackets = new Map(); + + +stubPreparedMessages.set("GET request", new NetworkEventMessage({ + "id": "1", + "actor": "server1.conn0.child1/netEvent29", + "level": "log", + "isXHR": false, + "request": { + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "GET" + }, + "response": {}, + "source": "network", + "type": "log", + "groupId": null +})); + +stubPreparedMessages.set("XHR GET request", new NetworkEventMessage({ + "id": "1", + "actor": "server1.conn1.child1/netEvent29", + "level": "log", + "isXHR": true, + "request": { + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "GET" + }, + "response": {}, + "source": "network", + "type": "log", + "groupId": null +})); + +stubPreparedMessages.set("XHR POST request", new NetworkEventMessage({ + "id": "1", + "actor": "server1.conn2.child1/netEvent29", + "level": "log", + "isXHR": true, + "request": { + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "POST" + }, + "response": {}, + "source": "network", + "type": "log", + "groupId": null +})); + + +stubPackets.set("GET request", { + "from": "server1.conn0.child1/consoleActor2", + "type": "networkEvent", + "eventActor": { + "actor": "server1.conn0.child1/netEvent29", + "startedDateTime": "2016-10-15T23:12:04.196Z", + "timeStamp": 1476573124196, + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "GET", + "isXHR": false, + "cause": { + "type": 3, + "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html", + "stacktrace": [ + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js", + "lineNumber": 3, + "columnNumber": 1, + "functionName": "triggerPacket", + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval", + "lineNumber": 4, + "columnNumber": 7, + "functionName": null, + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js", + "lineNumber": 53, + "columnNumber": 20, + "functionName": null, + "asyncCause": null + } + ] + }, + "private": false + } +}); + +stubPackets.set("XHR GET request", { + "from": "server1.conn1.child1/consoleActor2", + "type": "networkEvent", + "eventActor": { + "actor": "server1.conn1.child1/netEvent29", + "startedDateTime": "2016-10-15T23:12:05.690Z", + "timeStamp": 1476573125690, + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "GET", + "isXHR": true, + "cause": { + "type": 11, + "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html", + "stacktrace": [ + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js", + "lineNumber": 4, + "columnNumber": 1, + "functionName": "triggerPacket", + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval", + "lineNumber": 4, + "columnNumber": 7, + "functionName": null, + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js", + "lineNumber": 53, + "columnNumber": 20, + "functionName": null, + "asyncCause": null + } + ] + }, + "private": false + } +}); + +stubPackets.set("XHR POST request", { + "from": "server1.conn2.child1/consoleActor2", + "type": "networkEvent", + "eventActor": { + "actor": "server1.conn2.child1/netEvent29", + "startedDateTime": "2016-10-15T23:12:07.158Z", + "timeStamp": 1476573127158, + "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html", + "method": "POST", + "isXHR": true, + "cause": { + "type": 11, + "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html", + "stacktrace": [ + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js", + "lineNumber": 4, + "columnNumber": 1, + "functionName": "triggerPacket", + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval", + "lineNumber": 4, + "columnNumber": 7, + "functionName": null, + "asyncCause": null + }, + { + "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js", + "lineNumber": 53, + "columnNumber": 20, + "functionName": null, + "asyncCause": null + } + ] + }, + "private": false + } +}); + + +module.exports = { + stubPreparedMessages, + stubPackets, +}
\ No newline at end of file diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js new file mode 100644 index 0000000000..eda8e8b837 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE. + */ + +const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types"); + +let stubPreparedMessages = new Map(); +let stubPackets = new Map(); + + +stubPreparedMessages.set("ReferenceError: asdf is not defined", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "javascript", + "type": "log", + "level": "error", + "messageText": "ReferenceError: asdf is not defined", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"log\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":3,\"columnNumber\":5,\"functionName\":\"bar\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":6,\"columnNumber\":5,\"functionName\":\"foo\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":9,\"columnNumber\":3,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"line\":3,\"column\":5},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\"}", + "stacktrace": [ + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 3, + "columnNumber": 5, + "functionName": "bar" + }, + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 6, + "columnNumber": 5, + "functionName": "foo" + }, + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 9, + "columnNumber": 3, + "functionName": null + } + ], + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "line": 3, + "column": 5 + }, + "groupId": null, + "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default" +})); + + +stubPackets.set("ReferenceError: asdf is not defined", { + "from": "server1.conn0.child1/consoleActor2", + "type": "pageError", + "pageError": { + "errorMessage": "ReferenceError: asdf is not defined", + "errorMessageName": "JSMSG_NOT_DEFINED", + "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default", + "sourceName": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineText": "", + "lineNumber": 3, + "columnNumber": 5, + "category": "content javascript", + "timeStamp": 1476573167137, + "warning": false, + "error": false, + "exception": true, + "strict": false, + "info": false, + "private": false, + "stacktrace": [ + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 3, + "columnNumber": 5, + "functionName": "bar" + }, + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 6, + "columnNumber": 5, + "functionName": "foo" + }, + { + "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error", + "lineNumber": 9, + "columnNumber": 3, + "functionName": null + } + ] + } +}); + + +module.exports = { + stubPreparedMessages, + stubPackets, +}
\ No newline at end of file diff --git a/devtools/client/webconsole/new-console-output/test/helpers.js b/devtools/client/webconsole/new-console-output/test/helpers.js new file mode 100644 index 0000000000..39807eaeda --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/helpers.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let ReactDOM = require("devtools/client/shared/vendor/react-dom"); +let React = require("devtools/client/shared/vendor/react"); +var TestUtils = React.addons.TestUtils; + +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); +const { configureStore } = require("devtools/client/webconsole/new-console-output/store"); +const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator"); +const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); + +/** + * Prepare actions for use in testing. + */ +function setupActions() { + // Some actions use dependency injection. This helps them avoid using state in + // a hard-to-test way. We need to inject stubbed versions of these dependencies. + const wrappedActions = Object.assign({}, actions); + + const idGenerator = new IdGenerator(); + wrappedActions.messageAdd = (packet) => { + return actions.messageAdd(packet, idGenerator); + }; + + return wrappedActions; +} + +/** + * Prepare the store for use in testing. + */ +function setupStore(input) { + const store = configureStore(); + + // Add the messages from the input commands to the store. + input.forEach((cmd) => { + store.dispatch(actions.messageAdd(stubPackets.get(cmd))); + }); + + return store; +} + +function renderComponent(component, props) { + const el = React.createElement(component, props, {}); + // By default, renderIntoDocument() won't work for stateless components, but + // it will work if the stateless component is wrapped in a stateful one. + // See https://github.com/facebook/react/issues/4839 + const wrappedEl = React.DOM.span({}, [el]); + const renderedComponent = TestUtils.renderIntoDocument(wrappedEl); + return ReactDOM.findDOMNode(renderedComponent).children[0]; +} + +function shallowRenderComponent(component, props) { + const el = React.createElement(component, props); + const renderer = TestUtils.createRenderer(); + renderer.render(el, {}); + return renderer.getRenderOutput(); +} + +module.exports = { + setupActions, + setupStore, + renderComponent, + shallowRenderComponent +}; diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini new file mode 100644 index 0000000000..9881d05593 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini @@ -0,0 +1,21 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + test-batching.html + test-console.html + test-console-filters.html + test-console-group.html + test-console-table.html + !/devtools/client/framework/test/shared-head.js + +[browser_webconsole_batching.js] +[browser_webconsole_console_group.js] +[browser_webconsole_console_table.js] +[browser_webconsole_filters.js] +[browser_webconsole_init.js] +[browser_webconsole_input_focus.js] +[browser_webconsole_keyboard_accessibility.js] +[browser_webconsole_observer_notifications.js] +[browser_webconsole_vview_close_on_esc_key.js] diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js new file mode 100644 index 0000000000..0bfdccc3cd --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js @@ -0,0 +1,51 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +// Check adding console calls as batch keep the order of the message. + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html"; +const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages"); + +add_task(function* () { + let hud = yield openNewTabAndConsole(TEST_URI); + const messageNumber = 100; + yield testSimpleBatchLogging(hud, messageNumber); + yield testBatchLoggingAndClear(hud, messageNumber); +}); + +function* testSimpleBatchLogging(hud, messageNumber) { + yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber, + function (numMessages) { + content.wrappedJSObject.batchLog(numMessages); + } + ); + + for (let i = 0; i < messageNumber; i++) { + let node = yield waitFor(() => findMessageAtIndex(hud, i, i)); + is(node.textContent, i.toString(), `message at index "${i}" is the expected one`); + } +} + +function* testBatchLoggingAndClear(hud, messageNumber) { + yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber, + function (numMessages) { + content.wrappedJSObject.batchLogAndClear(numMessages); + } + ); + yield waitFor(() => findMessage(hud, l10n.getStr("consoleCleared"))); + ok(true, "console cleared message is displayed"); + + // Passing the text argument as an empty string will returns all the message, + // whatever their content is. + const messages = findMessages(hud, ""); + is(messages.length, 1, "console was cleared as expected"); +} + +function findMessageAtIndex(hud, text, index) { + const selector = `.message:nth-of-type(${index + 1}) .message-body`; + return findMessage(hud, text, selector); +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js new file mode 100644 index 0000000000..94de78f136 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +// Check console.group, console.groupCollapsed and console.groupEnd calls +// behave as expected. + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html"; +const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent"); + +add_task(function* () { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let hud = toolbox.getCurrentPanel().hud; + + const store = hud.ui.newConsoleOutput.getStore(); + // Adding loggin each time the store is modified in order to check + // the store state in case of failure. + store.subscribe(() => { + const messages = store.getState().messages.messagesById.toJS() + .map(message => { + return { + id: message.id, + type: message.type, + parameters: message.parameters, + messageText: message.messageText + }; + } + ); + info("messages : " + JSON.stringify(messages)); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function () { + content.wrappedJSObject.doLog(); + }); + + info("Test a group at root level"); + let node = yield waitFor(() => findMessage(hud, "group-1")); + testClass(node, "startGroup"); + testIndent(node, 0); + + info("Test a message in a 1 level deep group"); + node = yield waitFor(() => findMessage(hud, "log-1")); + testClass(node, "log"); + testIndent(node, 1); + + info("Test a group in a 1 level deep group"); + node = yield waitFor(() => findMessage(hud, "group-2")); + testClass(node, "startGroup"); + testIndent(node, 1); + + info("Test a message in a 2 level deep group"); + node = yield waitFor(() => findMessage(hud, "log-2")); + testClass(node, "log"); + testIndent(node, 2); + + info("Test a message in a 1 level deep group, after closing a 2 level deep group"); + node = yield waitFor(() => findMessage(hud, "log-3")); + testClass(node, "log"); + testIndent(node, 1); + + info("Test a message at root level, after closing all the groups"); + node = yield waitFor(() => findMessage(hud, "log-4")); + testClass(node, "log"); + testIndent(node, 0); + + info("Test a collapsed group at root level"); + node = yield waitFor(() => findMessage(hud, "group-3")); + testClass(node, "startGroupCollapsed"); + testIndent(node, 0); + + info("Test a message at root level, after closing a collapsed group"); + node = yield waitFor(() => findMessage(hud, "log-6")); + testClass(node, "log"); + testIndent(node, 0); + + let nodes = hud.ui.experimentalOutputNode.querySelectorAll(".message"); + is(nodes.length, 8, "expected number of messages are displayed"); +}); + +function testClass(node, className) { + ok(node.classList.contains(className), `message has the expected "${className}" class`); +} + +function testIndent(node, indent) { + indent = `${indent * INDENT_WIDTH}px`; + is(node.querySelector(".indent").style.width, indent, + "message has the expected level of indentation"); +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js new file mode 100644 index 0000000000..a90ae1af1b --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js @@ -0,0 +1,173 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +// Check console.table calls with all the test cases shown +// in the MDN doc (https://developer.mozilla.org/en-US/docs/Web/API/Console/table) + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html"; + +add_task(function* () { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let hud = toolbox.getCurrentPanel().hud; + + function Person(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + const testCases = [{ + info: "Testing when data argument is an array", + input: ["apples", "oranges", "bananas"], + expected: { + columns: ["(index)", "Values"], + rows: [ + ["0", "apples"], + ["1", "oranges"], + ["2", "bananas"], + ] + } + }, { + info: "Testing when data argument is an object", + input: new Person("John", "Smith"), + expected: { + columns: ["(index)", "Values"], + rows: [ + ["firstName", "John"], + ["lastName", "Smith"], + ] + } + }, { + info: "Testing when data argument is an array of arrays", + input: [["Jane", "Doe"], ["Emily", "Jones"]], + expected: { + columns: ["(index)", "0", "1"], + rows: [ + ["0", "Jane", "Doe"], + ["1", "Emily", "Jones"], + ] + } + }, { + info: "Testing when data argument is an array of objects", + input: [ + new Person("Jack", "Foo"), + new Person("Emma", "Bar"), + new Person("Michelle", "Rax"), + ], + expected: { + columns: ["(index)", "firstName", "lastName"], + rows: [ + ["0", "Jack", "Foo"], + ["1", "Emma", "Bar"], + ["2", "Michelle", "Rax"], + ] + } + }, { + info: "Testing when data argument is an object whose properties are objects", + input: { + father: new Person("Darth", "Vader"), + daughter: new Person("Leia", "Organa"), + son: new Person("Luke", "Skywalker"), + }, + expected: { + columns: ["(index)", "firstName", "lastName"], + rows: [ + ["father", "Darth", "Vader"], + ["daughter", "Leia", "Organa"], + ["son", "Luke", "Skywalker"], + ] + } + }, { + info: "Testing when data argument is a Set", + input: new Set(["a", "b", "c"]), + expected: { + columns: ["(iteration index)", "Values"], + rows: [ + ["0", "a"], + ["1", "b"], + ["2", "c"], + ] + } + }, { + info: "Testing when data argument is a Map", + input: new Map([["key-a", "value-a"], ["key-b", "value-b"]]), + expected: { + columns: ["(iteration index)", "Key", "Values"], + rows: [ + ["0", "key-a", "value-a"], + ["1", "key-b", "value-b"], + ] + } + }, { + info: "Testing restricting the columns displayed", + input: [ + new Person("Sam", "Wright"), + new Person("Elena", "Bartz"), + ], + headers: ["firstName"], + expected: { + columns: ["(index)", "firstName"], + rows: [ + ["0", "Sam"], + ["1", "Elena"], + ] + } + }]; + + yield ContentTask.spawn(gBrowser.selectedBrowser, testCases, function (tests) { + tests.forEach((test) => { + content.wrappedJSObject.doConsoleTable(test.input, test.headers); + }); + }); + + let nodes = []; + for (let testCase of testCases) { + let node = yield waitFor( + () => findConsoleTable(hud.ui.experimentalOutputNode, testCases.indexOf(testCase)) + ); + nodes.push(node); + } + + let consoleTableNodes = hud.ui.experimentalOutputNode.querySelectorAll( + ".message .new-consoletable"); + + is(consoleTableNodes.length, testCases.length, + "console has the expected number of consoleTable items"); + + testCases.forEach((testCase, index) => { + info(testCase.info); + + let node = nodes[index]; + let columns = Array.from(node.querySelectorAll("thead th")); + let rows = Array.from(node.querySelectorAll("tbody tr")); + + is( + JSON.stringify(testCase.expected.columns), + JSON.stringify(columns.map(column => column.textContent)), + "table has the expected columns" + ); + + is(testCase.expected.rows.length, rows.length, + "table has the expected number of rows"); + + testCase.expected.rows.forEach((expectedRow, rowIndex) => { + let row = rows[rowIndex]; + let cells = row.querySelectorAll("td"); + is(expectedRow.length, cells.length, "row has the expected number of cells"); + + expectedRow.forEach((expectedCell, cellIndex) => { + let cell = cells[cellIndex]; + is(expectedCell, cell.textContent, "cell has the expected content"); + }); + }); + }); +}); + +function findConsoleTable(node, index) { + let condition = node.querySelector( + `.message:nth-of-type(${index + 1}) .new-consoletable`); + return condition; +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js new file mode 100644 index 0000000000..8eb5369261 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js @@ -0,0 +1,72 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 filters. + +"use strict"; + +const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants"); + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html"; + +add_task(function* () { + let hud = yield openNewTabAndConsole(TEST_URI); + const outputNode = hud.ui.experimentalOutputNode; + + const toolbar = yield waitFor(() => { + return outputNode.querySelector(".webconsole-filterbar-primary"); + }); + ok(toolbar, "Toolbar found"); + + // Show the filter bar + toolbar.querySelector(".devtools-filter-icon").click(); + const filterBar = yield waitFor(() => { + return outputNode.querySelector(".webconsole-filterbar-secondary"); + }); + ok(filterBar, "Filter bar is shown when filter icon is clicked."); + + // Check defaults. + Object.values(MESSAGE_LEVEL).forEach(level => { + ok(filterIsEnabled(filterBar.querySelector(`.${level}`)), + `Filter button for ${level} is on by default`); + }); + ["net", "netxhr"].forEach(category => { + ok(!filterIsEnabled(filterBar.querySelector(`.${category}`)), + `Filter button for ${category} is off by default`); + }); + + // Check that messages are shown as expected. This depends on cached messages being + // shown. + ok(findMessages(hud, "").length == 5, + "Messages of all levels shown when filters are on."); + + // Check that messages are not shown when their filter is turned off. + filterBar.querySelector(".error").click(); + yield waitFor(() => findMessages(hud, "").length == 4); + ok(true, "When a filter is turned off, its messages are not shown."); + + // Check that the ui settings were persisted. + yield closeTabAndToolbox(); + yield testFilterPersistence(); +}); + +function filterIsEnabled(button) { + return button.classList.contains("checked"); +} + +function* testFilterPersistence() { + let hud = yield openNewTabAndConsole(TEST_URI); + const outputNode = hud.ui.experimentalOutputNode; + const filterBar = yield waitFor(() => { + return outputNode.querySelector(".webconsole-filterbar-secondary"); + }); + ok(filterBar, "Filter bar ui setting is persisted."); + + // Check that the filter settings were persisted. + ok(!filterIsEnabled(filterBar.querySelector(".error")), + "Filter button setting is persisted"); + ok(findMessages(hud, "").length == 4, + "Messages of all levels shown when filters are on."); +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js new file mode 100644 index 0000000000..4280270dd0 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html"; + +add_task(function* () { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let hud = toolbox.getCurrentPanel().hud; + let {ui} = hud; + + ok(ui.jsterm, "jsterm exists"); + ok(ui.newConsoleOutput, "newConsoleOutput exists"); + + // @TODO: fix proptype errors + let receievedMessages = waitForMessages({ + hud, + messages: [{ + text: '0', + }, { + text: '1', + }, { + text: '2', + }], + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { + content.wrappedJSObject.doLogs(3); + }); + + yield receievedMessages; +}); diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js new file mode 100644 index 0000000000..7660df2380 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = + `data:text/html;charset=utf-8,Test input focused + <script> + console.log("console message 1"); + </script>`; + +add_task(function* () { + let hud = yield openNewTabAndConsole(TEST_URI); + hud.jsterm.clearOutput(); + + let inputNode = hud.jsterm.inputNode; + ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared"); + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.log("console message 2"); + }); + let msg = yield waitFor(() => findMessage(hud, "console message 2")); + let outputItem = msg.querySelector(".message-body"); + + inputNode = hud.jsterm.inputNode; + ok(inputNode.getAttribute("focused"), "input node is focused, first"); + + yield waitForBlurredInput(inputNode); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + ok(inputNode.getAttribute("focused"), "input node is focused, second time"); + + yield waitForBlurredInput(inputNode); + + info("Setting a text selection and making sure a click does not re-focus"); + let selection = hud.iframeWindow.getSelection(); + selection.selectAllChildren(outputItem); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + ok(!inputNode.getAttribute("focused"), + "input node focused after text is selected"); +}); + +function waitForBlurredInput(inputNode) { + return new Promise(resolve => { + let lostFocus = () => { + ok(!inputNode.getAttribute("focused"), "input node is not focused"); + resolve(); + }; + inputNode.addEventListener("blur", lostFocus, { once: true }); + document.getElementById("urlbar").click(); + }); +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js new file mode 100644 index 0000000000..1038194b98 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js @@ -0,0 +1,71 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that basic keyboard shortcuts work in the web console. + +"use strict"; + +const TEST_URI = + `data:text/html;charset=utf-8,<p>Test keyboard accessibility</p> + <script> + for (let i = 1; i <= 100; i++) { + console.log("console message " + i); + } + </script> + `; + +add_task(function* () { + let hud = yield openNewTabAndConsole(TEST_URI); + info("Web Console opened"); + + const outputScroller = hud.ui.outputScroller; + + yield waitFor(() => findMessages(hud, "").length == 100); + + let currentPosition = outputScroller.scrollTop; + const bottom = currentPosition; + + EventUtils.sendMouseEvent({type: "click"}, hud.jsterm.inputNode); + + // Page up. + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + isnot(outputScroller.scrollTop, currentPosition, + "scroll position changed after page up"); + + // Page down. + currentPosition = outputScroller.scrollTop; + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + ok(outputScroller.scrollTop > currentPosition, + "scroll position now at bottom"); + + // Home + EventUtils.synthesizeKey("VK_HOME", {}); + is(outputScroller.scrollTop, 0, "scroll position now at top"); + + // End + EventUtils.synthesizeKey("VK_END", {}); + let scrollTop = outputScroller.scrollTop; + ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5, + "scroll position now at bottom"); + + // Clear output + info("try ctrl-l to clear output"); + let clearShortcut; + if (Services.appinfo.OS === "Darwin") { + clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX"); + } else { + clearShortcut = WCUL10n.getStr("webconsole.clear.key"); + } + synthesizeKeyShortcut(clearShortcut); + yield waitFor(() => findMessages(hud, "").length == 0); + is(hud.jsterm.inputNode.getAttribute("focused"), "true", "jsterm input is focused"); + + // Focus filter + info("try ctrl-f to focus filter"); + synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key")); + ok(!hud.jsterm.inputNode.getAttribute("focused"), "jsterm input is not focused"); + is(hud.ui.filterBox, outputScroller.ownerDocument.activeElement, + "filter input is focused"); +}); diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js new file mode 100644 index 0000000000..5225a6ac17 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " + + "obeserver notifications"; + +let created = false; +let destroyed = false; + +add_task(function* () { + setupObserver(); + yield openNewTabAndConsole(TEST_URI); + yield waitFor(() => created); + + yield closeTabAndToolbox(gBrowser.selectedTab); + yield waitFor(() => destroyed); +}); + +function setupObserver() { + const observer = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function observe(subject, topic) { + subject = subject.QueryInterface(Ci.nsISupportsString); + + switch (topic) { + case "web-console-created": + ok(HUDService.getHudReferenceById(subject.data), "We have a hud reference"); + Services.obs.removeObserver(observer, "web-console-created"); + created = true; + break; + case "web-console-destroyed": + ok(!HUDService.getHudReferenceById(subject.data), "We do not have a hud reference"); + Services.obs.removeObserver(observer, "web-console-destroyed"); + destroyed = true; + break; + } + }, + }; + + Services.obs.addObserver(observer, "web-console-created", false); + Services.obs.addObserver(observer, "web-console-destroyed", false); +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js new file mode 100644 index 0000000000..712a990b44 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js @@ -0,0 +1,46 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that the variables view sidebar can be closed by pressing Escape in the +// web console. + +"use strict"; + +const TEST_URI = + "data:text/html;charset=utf8,<script>let fooObj = {testProp: 'testValue'}</script>"; + +add_task(function* () { + let hud = yield openNewTabAndConsole(TEST_URI); + let jsterm = hud.jsterm; + let vview; + + yield openSidebar("fooObj", 'testProp: "testValue"'); + vview.window.focus(); + + let sidebarClosed = jsterm.once("sidebar-closed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield sidebarClosed; + + function* openSidebar(objName, expectedText) { + yield jsterm.execute(objName); + info("JSTerm executed"); + + let msg = yield waitFor(() => findMessage(hud, "Object")); + ok(msg, "Message found"); + + let anchor = msg.querySelector("a"); + let body = msg.querySelector(".message-body"); + ok(anchor, "object anchor"); + ok(body, "message body"); + ok(body.textContent.includes(expectedText), "message text check"); + + msg.scrollIntoView(); + yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow); + + let vviewVar = yield jsterm.once("variablesview-fetched"); + vview = vviewVar._variablesView; + ok(vview, "variables view object exists"); + } +}); diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/head.js b/devtools/client/webconsole/new-console-output/test/mochitest/head.js new file mode 100644 index 0000000000..b71eaec4f1 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ +/* import-globals-from ../../../../framework/test/shared-head.js */ + +"use strict"; + +// shared-head.js handles imports, constants, and utility functions +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", + this); + +var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils"); +const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties"; +var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI); + +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); + + let browserConsole = HUDService.getBrowserConsole(); + if (browserConsole) { + if (browserConsole.jsterm) { + browserConsole.jsterm.clearOutput(true); + } + yield HUDService.toggleBrowserConsole(); + } +}); + +/** + * Add a new tab and open the toolbox in it, and select the webconsole. + * + * @param string url + * The URL for the tab to be opened. + * @return Promise + * Resolves when the tab has been added, loaded and the toolbox has been opened. + * Resolves to the toolbox. + */ +var openNewTabAndConsole = Task.async(function* (url) { + let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm._lazyVariablesView = false; + return hud; +}); + +/** + * Wait for messages in the web console output, resolving once they are receieved. + * + * @param object options + * - hud: the webconsole + * - messages: Array[Object]. An array of messages to match. Current supported options: + * - text: Exact text match in .message-body + */ +function waitForMessages({ hud, messages }) { + return new Promise(resolve => { + let numMatched = 0; + let receivedLog = hud.ui.on("new-messages", function messagesReceieved(e, newMessages) { + for (let message of messages) { + if (message.matched) { + continue; + } + + for (let newMessage of newMessages) { + if (newMessage.node.querySelector(".message-body").textContent == message.text) { + numMatched++; + message.matched = true; + info("Matched a message with text: " + message.text + ", still waiting for " + (messages.length - numMatched) + " messages"); + break; + } + } + + if (numMatched === messages.length) { + hud.ui.off("new-messages", messagesReceieved); + resolve(receivedLog); + return; + } + } + }); + }); +} + +/** + * Wait for a predicate to return a result. + * + * @param function condition + * Invoked once in a while until it returns a truthy value. This should be an + * idempotent function, since we have to run it a second time after it returns + * true in order to return the value. + * @param string message [optional] + * A message to output if the condition failes. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + * @return object + * A promise that is resolved with the result of the condition. + */ +function* waitFor(condition, message = "waitFor", interval = 10, maxTries = 500) { + return new Promise(resolve => { + BrowserTestUtils.waitForCondition(condition, message, interval, maxTries) + .then(() => resolve(condition())); + }); +} + +/** + * Find a message in the output. + * + * @param object hud + * The web console. + * @param string text + * A substring that can be found in the message. + * @param selector [optional] + * The selector to use in finding the message. + */ +function findMessage(hud, text, selector = ".message") { + const elements = findMessages(hud, text, selector); + return elements.pop(); +} + +/** + * Find multiple messages in the output. + * + * @param object hud + * The web console. + * @param string text + * A substring that can be found in the message. + * @param selector [optional] + * The selector to use in finding the message. + */ +function findMessages(hud, text, selector = ".message") { + const messages = hud.ui.experimentalOutputNode.querySelectorAll(selector); + const elements = Array.prototype.filter.call( + messages, + (el) => el.textContent.includes(text) + ); + return elements; +} diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html new file mode 100644 index 0000000000..9d122387a2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole batch console calls test page</title> + </head> + <body> + <p>batch console calls test page</p> + <script> + "use strict"; + + function batchLog(numMessages = 0) { + for (let i = 0; i < numMessages; i++) { + console.log(i); + } + } + + function batchLogAndClear(numMessages = 0) { + for (let i = 0; i < numMessages; i++) { + console.log(i); + if (i === numMessages - 1) { + console.clear(); + } + } + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html new file mode 100644 index 0000000000..2934215495 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole filters test page</title> + </head> + <body> + <p>Webconsole filters test page</p> + <script> + console.log("console log"); + console.warn("console warn"); + console.error("console error"); + console.info("console info"); + console.count("console debug"); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html new file mode 100644 index 0000000000..47373d3b92 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Webconsole console.group test page</title> + </head> + <body> + <p>console.group() & console.groupCollapsed() test page</p> + <script> + "use strict"; + + function doLog() { + console.group("group-1"); + console.log("log-1"); + console.group("group-2"); + console.log("log-2"); + console.groupEnd("group-2"); + console.log("log-3"); + console.groupEnd("group-1"); + console.log("log-4"); + console.groupCollapsed("group-3"); + console.log("log-5"); + console.groupEnd("group-3"); + console.log("log-6"); + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html new file mode 100644 index 0000000000..b7666e50b0 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Simple webconsole test page</title> + </head> + <body> + <p>console.table() test page</p> + <script> + function doConsoleTable(data, constrainedHeaders = null) { + if (constrainedHeaders) { + console.table(data, constrainedHeaders); + } else { + console.table(data); + } + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html new file mode 100644 index 0000000000..7ef09d9a10 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Simple webconsole test page</title> + </head> + <body> + <p>Simple webconsole test page</p> + <script> + function doLogs(num) { + num = num || 1; + for (var i = 0; i < num; i++) { + console.log(i); + } + } + </script> + </body> +</html> diff --git a/devtools/client/webconsole/new-console-output/test/moz.build b/devtools/client/webconsole/new-console-output/test/moz.build new file mode 100644 index 0000000000..da06c3162d --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/moz.build @@ -0,0 +1,17 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += [ + 'fixtures/stub-generators/browser.ini', + 'mochitest/browser.ini', +] + +DIRS += [ + 'fixtures' +] + +MOCHITEST_CHROME_MANIFESTS += [ + 'chrome/chrome.ini', +] diff --git a/devtools/client/webconsole/new-console-output/test/requireHelper.js b/devtools/client/webconsole/new-console-output/test/requireHelper.js new file mode 100644 index 0000000000..ac62058082 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/requireHelper.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const requireHacker = require("require-hacker"); + +requireHacker.global_hook("default", path => { + switch (path) { + // For Enzyme + case "react-dom/server": + return `const React = require('react-dev'); module.exports = React`; + case "react-addons-test-utils": + return `const React = require('react-dev'); module.exports = React.addons.TestUtils`; + // Use react-dev. This would be handled by browserLoader in Firefox. + case "react": + case "devtools/client/shared/vendor/react": + return `const React = require('react-dev'); module.exports = React`; + // For Rep's use of AMD + case "devtools/client/shared/vendor/react.default": + return `const React = require('react-dev'); module.exports = React`; + } + + // Some modules depend on Chrome APIs which don't work in mocha. When such a module + // is required, replace it with a mock version. + switch (path) { + case "devtools/client/webconsole/utils": + return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils")`; + case "devtools/shared/l10n": + return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper")`; + case "devtools/shared/plural-form": + return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/PluralForm")`; + case "Services": + case "Services.default": + return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/Services")`; + case "devtools/shared/client/main": + return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient")`; + } +}); diff --git a/devtools/client/webconsole/new-console-output/test/store/filters.test.js b/devtools/client/webconsole/new-console-output/test/store/filters.test.js new file mode 100644 index 0000000000..3c38a255a2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/store/filters.test.js @@ -0,0 +1,215 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const expect = require("expect"); + +const actions = require("devtools/client/webconsole/new-console-output/actions/index"); +const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/index"); +const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types"); +const { getAllMessages } = require("devtools/client/webconsole/new-console-output/selectors/messages"); +const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters"); +const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers"); +const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants"); +const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); + +describe("Filtering", () => { + let store; + let numMessages; + // Number of messages in prepareBaseStore which are not filtered out, i.e. Evaluation + // Results, console commands and console.groups . + const numUnfilterableMessages = 3; + + beforeEach(() => { + store = prepareBaseStore(); + store.dispatch(actions.filtersClear()); + numMessages = getAllMessages(store.getState()).size; + }); + + describe("Level filter", () => { + it("filters log messages", () => { + store.dispatch(actions.filterToggle(MESSAGE_LEVEL.LOG)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages - 3); + }); + + it("filters debug messages", () => { + store.dispatch(actions.filterToggle(MESSAGE_LEVEL.DEBUG)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages - 1); + }); + + // @TODO add info stub + it("filters info messages"); + + it("filters warning messages", () => { + store.dispatch(actions.filterToggle(MESSAGE_LEVEL.WARN)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages - 1); + }); + + it("filters error messages", () => { + store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages - 1); + }); + + it("filters xhr messages", () => { + let message = stubPreparedMessages.get("XHR GET request"); + store.dispatch(messageAdd(message)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages); + + store.dispatch(actions.filterToggle("netxhr")); + messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages + 1); + }); + + it("filters network messages", () => { + let message = stubPreparedMessages.get("GET request"); + store.dispatch(messageAdd(message)); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages); + + store.dispatch(actions.filterToggle("net")); + messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages + 1); + }); + }); + + describe("Text filter", () => { + it("matches on value grips", () => { + store.dispatch(actions.filterTextSet("danger")); + + let messages = getAllMessages(store.getState()); + expect(messages.size - numUnfilterableMessages).toEqual(1); + }); + + it("matches unicode values", () => { + store.dispatch(actions.filterTextSet("鼬")); + + let messages = getAllMessages(store.getState()); + expect(messages.size - numUnfilterableMessages).toEqual(1); + }); + + it("matches locations", () => { + // Add a message with a different filename. + let locationMsg = + Object.assign({}, stubPackets.get("console.log('foobar', 'test')")); + locationMsg.message = + Object.assign({}, locationMsg.message, { filename: "search-location-test.js" }); + store.dispatch(messageAdd(locationMsg)); + + store.dispatch(actions.filterTextSet("search-location-test.js")); + + let messages = getAllMessages(store.getState()); + expect(messages.size - numUnfilterableMessages).toEqual(1); + }); + + it("matches stacktrace functionName", () => { + let traceMessage = stubPackets.get("console.trace()"); + store.dispatch(messageAdd(traceMessage)); + + store.dispatch(actions.filterTextSet("testStacktraceFiltering")); + + let messages = getAllMessages(store.getState()); + expect(messages.size - numUnfilterableMessages).toEqual(1); + }); + + it("matches stacktrace location", () => { + let traceMessage = stubPackets.get("console.trace()"); + traceMessage.message = + Object.assign({}, traceMessage.message, { + filename: "search-location-test.js", + lineNumber: 85, + columnNumber: 13 + }); + + store.dispatch(messageAdd(traceMessage)); + + store.dispatch(actions.filterTextSet("search-location-test.js:85:13")); + + let messages = getAllMessages(store.getState()); + expect(messages.size - numUnfilterableMessages).toEqual(1); + }); + + it("restores all messages once text is cleared", () => { + store.dispatch(actions.filterTextSet("danger")); + store.dispatch(actions.filterTextSet("")); + + let messages = getAllMessages(store.getState()); + expect(messages.size).toEqual(numMessages); + }); + }); + + describe("Combined filters", () => { + // @TODO add test + it("filters"); + }); +}); + +describe("Clear filters", () => { + it("clears all filters", () => { + const store = setupStore([]); + + // Setup test case + store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR)); + store.dispatch(actions.filterToggle("netxhr")); + store.dispatch(actions.filterTextSet("foobar")); + + let filters = getAllFilters(store.getState()); + expect(filters.toJS()).toEqual({ + "debug": true, + "error": false, + "info": true, + "log": true, + "net": false, + "netxhr": true, + "warn": true, + "text": "foobar" + }); + + store.dispatch(actions.filtersClear()); + + filters = getAllFilters(store.getState()); + expect(filters.toJS()).toEqual({ + "debug": true, + "error": true, + "info": true, + "log": true, + "net": false, + "netxhr": false, + "warn": true, + "text": "" + }); + }); +}); + +function prepareBaseStore() { + const store = setupStore([ + // Console API + "console.log('foobar', 'test')", + "console.warn('danger, will robinson!')", + "console.log(undefined)", + "console.count('bar')", + "console.log('鼬')", + // Evaluation Result - never filtered + "new Date(0)", + // PageError + "ReferenceError: asdf is not defined", + "console.group('bar')" + ]); + + // Console Command - never filtered + store.dispatch(messageAdd(new ConsoleCommand({ messageText: `console.warn("x")` }))); + + return store; +} diff --git a/devtools/client/webconsole/new-console-output/test/store/messages.test.js b/devtools/client/webconsole/new-console-output/test/store/messages.test.js new file mode 100644 index 0000000000..582ca36e30 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js @@ -0,0 +1,353 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { + getAllMessages, + getAllMessagesUiById, + getAllGroupsById, + getCurrentGroup, +} = require("devtools/client/webconsole/new-console-output/selectors/messages"); +const { + setupActions, + setupStore +} = require("devtools/client/webconsole/new-console-output/test/helpers"); +const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); +const { + MESSAGE_TYPE, +} = require("devtools/client/webconsole/new-console-output/constants"); + +const expect = require("expect"); + +describe("Message reducer:", () => { + let actions; + + before(() => { + actions = setupActions(); + }); + + describe("messagesById", () => { + it("adds a message to an empty store", () => { + const { dispatch, getState } = setupStore([]); + + const packet = stubPackets.get("console.log('foobar', 'test')"); + const message = stubPreparedMessages.get("console.log('foobar', 'test')"); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + + expect(messages.first()).toEqual(message); + }); + + it("increments repeat on a repeating message", () => { + const { dispatch, getState } = setupStore([ + "console.log('foobar', 'test')", + "console.log('foobar', 'test')" + ]); + + const packet = stubPackets.get("console.log('foobar', 'test')"); + dispatch(actions.messageAdd(packet)); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + + expect(messages.size).toBe(1); + expect(messages.first().repeat).toBe(4); + }); + + it("does not clobber a unique message", () => { + const { dispatch, getState } = setupStore([ + "console.log('foobar', 'test')", + "console.log('foobar', 'test')" + ]); + + const packet = stubPackets.get("console.log('foobar', 'test')"); + dispatch(actions.messageAdd(packet)); + + const packet2 = stubPackets.get("console.log(undefined)"); + dispatch(actions.messageAdd(packet2)); + + const messages = getAllMessages(getState()); + + expect(messages.size).toBe(2); + expect(messages.first().repeat).toBe(3); + expect(messages.last().repeat).toBe(1); + }); + + it("adds a message in response to console.clear()", () => { + const { dispatch, getState } = setupStore([]); + + dispatch(actions.messageAdd(stubPackets.get("console.clear()"))); + + const messages = getAllMessages(getState()); + + expect(messages.size).toBe(1); + expect(messages.first().parameters[0]).toBe("Console was cleared."); + }); + + it("clears the messages list in response to MESSAGES_CLEAR action", () => { + const { dispatch, getState } = setupStore([ + "console.log('foobar', 'test')", + "console.log(undefined)" + ]); + + dispatch(actions.messagesClear()); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(0); + }); + + it("limits the number of messages displayed", () => { + const { dispatch, getState } = setupStore([]); + + const logLimit = 1000; + const packet = stubPackets.get("console.log(undefined)"); + for (let i = 1; i <= logLimit + 1; i++) { + packet.message.arguments = [`message num ${i}`]; + dispatch(actions.messageAdd(packet)); + } + + const messages = getAllMessages(getState()); + expect(messages.count()).toBe(logLimit); + expect(messages.first().parameters[0]).toBe(`message num 2`); + expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`); + }); + + it("does not add null messages to the store", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.time('bar')"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(0); + }); + + it("adds console.table call with unsupported type as console.log", () => { + const { dispatch, getState } = setupStore([]); + + const packet = stubPackets.get("console.table('bar')"); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + const tableMessage = messages.last(); + expect(tableMessage.level).toEqual(MESSAGE_TYPE.LOG); + }); + + it("adds console.group messages to the store", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.group('bar')"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(1); + }); + + it("sets groupId property as expected", () => { + const { dispatch, getState } = setupStore([]); + + dispatch(actions.messageAdd( + stubPackets.get("console.group('bar')"))); + + const packet = stubPackets.get("console.log('foobar', 'test')"); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(2); + expect(messages.last().groupId).toBe(messages.first().id); + }); + + it("does not display console.groupEnd messages to the store", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.groupEnd('bar')"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(0); + }); + + it("filters out message added after a console.groupCollapsed message", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.groupCollapsed('foo')"); + dispatch(actions.messageAdd(message)); + + dispatch(actions.messageAdd( + stubPackets.get("console.log('foobar', 'test')"))); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(1); + }); + + it("shows the group of the first displayed message when messages are pruned", () => { + const { dispatch, getState } = setupStore([]); + + const logLimit = 1000; + + const groupMessage = stubPreparedMessages.get("console.group('bar')"); + dispatch(actions.messageAdd( + stubPackets.get("console.group('bar')"))); + + const packet = stubPackets.get("console.log(undefined)"); + for (let i = 1; i <= logLimit + 1; i++) { + packet.message.arguments = [`message num ${i}`]; + dispatch(actions.messageAdd(packet)); + } + + const messages = getAllMessages(getState()); + expect(messages.count()).toBe(logLimit); + expect(messages.first().messageText).toBe(groupMessage.messageText); + expect(messages.get(1).parameters[0]).toBe(`message num 3`); + expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`); + }); + + it("adds console.dirxml call as console.log", () => { + const { dispatch, getState } = setupStore([]); + + const packet = stubPackets.get("console.dirxml(window)"); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + const dirxmlMessage = messages.last(); + expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG); + }); + }); + + describe("messagesUiById", () => { + it("opens console.trace messages when they are added", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.trace()"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + const messagesUi = getAllMessagesUiById(getState()); + expect(messagesUi.size).toBe(1); + expect(messagesUi.first()).toBe(messages.first().id); + }); + + it("clears the messages UI list in response to MESSAGES_CLEAR action", () => { + const { dispatch, getState } = setupStore([ + "console.log('foobar', 'test')", + "console.log(undefined)" + ]); + + const traceMessage = stubPackets.get("console.trace()"); + dispatch(actions.messageAdd(traceMessage)); + + dispatch(actions.messagesClear()); + + const messagesUi = getAllMessagesUiById(getState()); + expect(messagesUi.size).toBe(0); + }); + + it("opens console.group messages when they are added", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.group('bar')"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + const messagesUi = getAllMessagesUiById(getState()); + expect(messagesUi.size).toBe(1); + expect(messagesUi.first()).toBe(messages.first().id); + }); + + it("does not open console.groupCollapsed messages when they are added", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubPackets.get("console.groupCollapsed('foo')"); + dispatch(actions.messageAdd(message)); + + const messagesUi = getAllMessagesUiById(getState()); + expect(messagesUi.size).toBe(0); + }); + }); + + describe("currentGroup", () => { + it("sets the currentGroup when console.group message is added", () => { + const { dispatch, getState } = setupStore([]); + + const packet = stubPackets.get("console.group('bar')"); + dispatch(actions.messageAdd(packet)); + + const messages = getAllMessages(getState()); + const currentGroup = getCurrentGroup(getState()); + expect(currentGroup).toBe(messages.first().id); + }); + + it("sets currentGroup to expected value when console.groupEnd is added", () => { + const { dispatch, getState } = setupStore([ + "console.group('bar')", + "console.groupCollapsed('foo')" + ]); + + let messages = getAllMessages(getState()); + let currentGroup = getCurrentGroup(getState()); + expect(currentGroup).toBe(messages.last().id); + + const endFooPacket = stubPackets.get("console.groupEnd('foo')"); + dispatch(actions.messageAdd(endFooPacket)); + messages = getAllMessages(getState()); + currentGroup = getCurrentGroup(getState()); + expect(currentGroup).toBe(messages.first().id); + + const endBarPacket = stubPackets.get("console.groupEnd('foo')"); + dispatch(actions.messageAdd(endBarPacket)); + messages = getAllMessages(getState()); + currentGroup = getCurrentGroup(getState()); + expect(currentGroup).toBe(null); + }); + + it("resets the currentGroup to null in response to MESSAGES_CLEAR action", () => { + const { dispatch, getState } = setupStore([ + "console.group('bar')" + ]); + + dispatch(actions.messagesClear()); + + const currentGroup = getCurrentGroup(getState()); + expect(currentGroup).toBe(null); + }); + }); + + describe("groupsById", () => { + it("adds the group with expected array when console.group message is added", () => { + const { dispatch, getState } = setupStore([]); + + const barPacket = stubPackets.get("console.group('bar')"); + dispatch(actions.messageAdd(barPacket)); + + let messages = getAllMessages(getState()); + let groupsById = getAllGroupsById(getState()); + expect(groupsById.size).toBe(1); + expect(groupsById.has(messages.first().id)).toBe(true); + expect(groupsById.get(messages.first().id)).toEqual([]); + + const fooPacket = stubPackets.get("console.groupCollapsed('foo')"); + dispatch(actions.messageAdd(fooPacket)); + messages = getAllMessages(getState()); + groupsById = getAllGroupsById(getState()); + expect(groupsById.size).toBe(2); + expect(groupsById.has(messages.last().id)).toBe(true); + expect(groupsById.get(messages.last().id)).toEqual([messages.first().id]); + }); + + it("resets groupsById in response to MESSAGES_CLEAR action", () => { + const { dispatch, getState } = setupStore([ + "console.group('bar')", + "console.groupCollapsed('foo')", + ]); + + let groupsById = getAllGroupsById(getState()); + expect(groupsById.size).toBe(2); + + dispatch(actions.messagesClear()); + + groupsById = getAllGroupsById(getState()); + expect(groupsById.size).toBe(0); + }); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js new file mode 100644 index 0000000000..d27238e143 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { getRepeatId } = require("devtools/client/webconsole/new-console-output/utils/messages"); +const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index"); + +const expect = require("expect"); + +describe("getRepeatId:", () => { + it("returns same repeatId for duplicate values", () => { + const message1 = stubPreparedMessages.get("console.log('foobar', 'test')"); + const message2 = message1.set("repeat", 3); + expect(getRepeatId(message1)).toEqual(getRepeatId(message2)); + }); + + it("returns different repeatIds for different values", () => { + const message1 = stubPreparedMessages.get("console.log('foobar', 'test')"); + const message2 = message1.set("parameters", ["funny", "monkey"]); + expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2)); + }); + + it("returns different repeatIds for different severities", () => { + const message1 = stubPreparedMessages.get("console.log('foobar', 'test')"); + const message2 = message1.set("level", "error"); + expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2)); + }); + + it("handles falsy values distinctly", () => { + const messageNaN = stubPreparedMessages.get("console.log(NaN)"); + const messageUnd = stubPreparedMessages.get("console.log(undefined)"); + const messageNul = stubPreparedMessages.get("console.log(null)"); + + const repeatIds = new Set([ + getRepeatId(messageNaN), + getRepeatId(messageUnd), + getRepeatId(messageNul)] + ); + expect(repeatIds.size).toEqual(3); + }); +}); diff --git a/devtools/client/webconsole/new-console-output/types.js b/devtools/client/webconsole/new-console-output/types.js new file mode 100644 index 0000000000..897ae5d3a4 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/types.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ +"use strict"; + +const Immutable = require("devtools/client/shared/vendor/immutable"); + +const { + MESSAGE_SOURCE, + MESSAGE_TYPE, + MESSAGE_LEVEL +} = require("devtools/client/webconsole/new-console-output/constants"); + +exports.ConsoleCommand = Immutable.Record({ + id: null, + allowRepeating: false, + messageText: null, + source: MESSAGE_SOURCE.JAVASCRIPT, + type: MESSAGE_TYPE.COMMAND, + level: MESSAGE_LEVEL.LOG, + groupId: null, +}); + +exports.ConsoleMessage = Immutable.Record({ + id: null, + allowRepeating: true, + source: null, + type: null, + level: null, + messageText: null, + parameters: null, + repeat: 1, + repeatId: null, + stacktrace: null, + frame: null, + groupId: null, + exceptionDocURL: null, + userProvidedStyles: null, +}); + +exports.NetworkEventMessage = Immutable.Record({ + id: null, + actor: null, + level: MESSAGE_LEVEL.LOG, + isXHR: false, + request: null, + response: null, + source: MESSAGE_SOURCE.NETWORK, + type: MESSAGE_TYPE.LOG, + groupId: null, +}); diff --git a/devtools/client/webconsole/new-console-output/utils/id-generator.js b/devtools/client/webconsole/new-console-output/utils/id-generator.js new file mode 100644 index 0000000000..7d875b7506 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/utils/id-generator.js @@ -0,0 +1,22 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +exports.IdGenerator = class IdGenerator { + constructor() { + this.messageId = 1; + } + + getNextId() { + // Return the next message id, as a string. + return "" + this.messageId++; + } + + getCurrentId() { + return this.messageId; + } +}; diff --git a/devtools/client/webconsole/new-console-output/utils/messages.js b/devtools/client/webconsole/new-console-output/utils/messages.js new file mode 100644 index 0000000000..f91209e9d2 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/utils/messages.js @@ -0,0 +1,283 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; +const l10n = new WebConsoleUtils.L10n(STRINGS_URI); + +const { + MESSAGE_SOURCE, + MESSAGE_TYPE, + MESSAGE_LEVEL, +} = require("../constants"); +const { + ConsoleMessage, + NetworkEventMessage, +} = require("../types"); + +function prepareMessage(packet, idGenerator) { + // This packet is already in the expected packet structure. Simply return. + if (!packet.source) { + packet = transformPacket(packet); + } + + if (packet.allowRepeating) { + packet = packet.set("repeatId", getRepeatId(packet)); + } + return packet.set("id", idGenerator.getNextId()); +} + +/** + * Transforms a packet from Firefox RDP structure to Chrome RDP structure. + */ +function transformPacket(packet) { + if (packet._type) { + packet = convertCachedPacket(packet); + } + + switch (packet.type) { + case "consoleAPICall": { + let { message } = packet; + + let parameters = message.arguments; + let type = message.level; + let level = getLevelFromType(type); + let messageText = null; + const timer = message.timer; + + // Special per-type conversion. + switch (type) { + case "clear": + // We show a message to users when calls console.clear() is called. + parameters = [l10n.getStr("consoleCleared")]; + break; + case "count": + // Chrome RDP doesn't have a special type for count. + type = MESSAGE_TYPE.LOG; + let {counter} = message; + let label = counter.label ? counter.label : l10n.getStr("noCounterLabel"); + messageText = `${label}: ${counter.count}`; + parameters = null; + break; + case "time": + // We don't show anything for console.time calls to match Chrome's behaviour. + parameters = null; + type = MESSAGE_TYPE.NULL_MESSAGE; + break; + case "timeEnd": + parameters = null; + if (timer) { + // We show the duration to users when calls console.timeEnd() is called, + // if corresponding console.time() was called before. + let duration = Math.round(timer.duration * 100) / 100; + messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]); + } else { + // If the `timer` property does not exists, we don't output anything. + type = MESSAGE_TYPE.NULL_MESSAGE; + } + break; + case "table": + const supportedClasses = [ + "Array", "Object", "Map", "Set", "WeakMap", "WeakSet"]; + if ( + !Array.isArray(parameters) || + parameters.length === 0 || + !supportedClasses.includes(parameters[0].class) + ) { + // If the class of the first parameter is not supported, + // we handle the call as a simple console.log + type = "log"; + } + break; + case "group": + type = MESSAGE_TYPE.START_GROUP; + parameters = null; + messageText = message.groupName || l10n.getStr("noGroupLabel"); + break; + case "groupCollapsed": + type = MESSAGE_TYPE.START_GROUP_COLLAPSED; + parameters = null; + messageText = message.groupName || l10n.getStr("noGroupLabel"); + break; + case "groupEnd": + type = MESSAGE_TYPE.END_GROUP; + parameters = null; + break; + case "dirxml": + // Handle console.dirxml calls as simple console.log + type = "log"; + break; + } + + const frame = message.filename ? { + source: message.filename, + line: message.lineNumber, + column: message.columnNumber, + } : null; + + return new ConsoleMessage({ + source: MESSAGE_SOURCE.CONSOLE_API, + type, + level, + parameters, + messageText, + stacktrace: message.stacktrace ? message.stacktrace : null, + frame, + userProvidedStyles: message.styles, + }); + } + + case "navigationMessage": { + let { message } = packet; + return new ConsoleMessage({ + source: MESSAGE_SOURCE.CONSOLE_API, + type: MESSAGE_TYPE.LOG, + level: MESSAGE_LEVEL.LOG, + messageText: "Navigated to " + message.url, + }); + } + + case "pageError": { + let { pageError } = packet; + let level = MESSAGE_LEVEL.ERROR; + if (pageError.warning || pageError.strict) { + level = MESSAGE_LEVEL.WARN; + } else if (pageError.info) { + level = MESSAGE_LEVEL.INFO; + } + + const frame = pageError.sourceName ? { + source: pageError.sourceName, + line: pageError.lineNumber, + column: pageError.columnNumber + } : null; + + return new ConsoleMessage({ + source: MESSAGE_SOURCE.JAVASCRIPT, + type: MESSAGE_TYPE.LOG, + level, + messageText: pageError.errorMessage, + stacktrace: pageError.stacktrace ? pageError.stacktrace : null, + frame, + exceptionDocURL: pageError.exceptionDocURL, + }); + } + + case "networkEvent": { + let { networkEvent } = packet; + + return new NetworkEventMessage({ + actor: networkEvent.actor, + isXHR: networkEvent.isXHR, + request: networkEvent.request, + response: networkEvent.response, + }); + } + + case "evaluationResult": + default: { + let { + exceptionMessage: messageText, + exceptionDocURL, + frame, + result: parameters + } = packet; + + const level = messageText ? MESSAGE_LEVEL.ERROR : MESSAGE_LEVEL.LOG; + return new ConsoleMessage({ + source: MESSAGE_SOURCE.JAVASCRIPT, + type: MESSAGE_TYPE.RESULT, + level, + messageText, + parameters, + exceptionDocURL, + frame, + }); + } + } +} + +// Helpers +function getRepeatId(message) { + message = message.toJS(); + delete message.repeat; + return JSON.stringify(message); +} + +function convertCachedPacket(packet) { + // The devtools server provides cached message packets in a different shape, so we + // transform them here. + let convertPacket = {}; + if (packet._type === "ConsoleAPI") { + convertPacket.message = packet; + convertPacket.type = "consoleAPICall"; + } else if (packet._type === "PageError") { + convertPacket.pageError = packet; + convertPacket.type = "pageError"; + } else if ("_navPayload" in packet) { + convertPacket.type = "navigationMessage"; + convertPacket.message = packet; + } else if (packet._type === "NetworkEvent") { + convertPacket.networkEvent = packet; + convertPacket.type = "networkEvent"; + } else { + throw new Error("Unexpected packet type"); + } + return convertPacket; +} + +/** + * Maps a Firefox RDP type to its corresponding level. + */ +function getLevelFromType(type) { + const levels = { + LEVEL_ERROR: "error", + LEVEL_WARNING: "warn", + LEVEL_INFO: "info", + LEVEL_LOG: "log", + LEVEL_DEBUG: "debug", + }; + + // A mapping from the console API log event levels to the Web Console levels. + const levelMap = { + error: levels.LEVEL_ERROR, + exception: levels.LEVEL_ERROR, + assert: levels.LEVEL_ERROR, + warn: levels.LEVEL_WARNING, + info: levels.LEVEL_INFO, + log: levels.LEVEL_LOG, + clear: levels.LEVEL_LOG, + trace: levels.LEVEL_LOG, + table: levels.LEVEL_LOG, + debug: levels.LEVEL_LOG, + dir: levels.LEVEL_LOG, + dirxml: levels.LEVEL_LOG, + group: levels.LEVEL_LOG, + groupCollapsed: levels.LEVEL_LOG, + groupEnd: levels.LEVEL_LOG, + time: levels.LEVEL_LOG, + timeEnd: levels.LEVEL_LOG, + count: levels.LEVEL_DEBUG, + }; + + return levelMap[type] || MESSAGE_TYPE.LOG; +} + +function isGroupType(type) { + return [ + MESSAGE_TYPE.START_GROUP, + MESSAGE_TYPE.START_GROUP_COLLAPSED + ].includes(type); +} + +exports.prepareMessage = prepareMessage; +// Export for use in testing. +exports.getRepeatId = getRepeatId; + +exports.l10n = l10n; +exports.isGroupType = isGroupType; diff --git a/devtools/client/webconsole/new-console-output/utils/moz.build b/devtools/client/webconsole/new-console-output/utils/moz.build new file mode 100644 index 0000000000..00378baa43 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/utils/moz.build @@ -0,0 +1,10 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'id-generator.js', + 'messages.js', + 'variables-view.js', +) diff --git a/devtools/client/webconsole/new-console-output/utils/variables-view.js b/devtools/client/webconsole/new-console-output/utils/variables-view.js new file mode 100644 index 0000000000..3cfee875a9 --- /dev/null +++ b/devtools/client/webconsole/new-console-output/utils/variables-view.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +/* global window */ +"use strict"; + +/** + * @TODO Remove this. + * + * Once JSTerm is also written in React/Redux, these will be actions. + */ +exports.openVariablesView = (objectActor) => { + window.jsterm.openVariablesView({ + objectActor, + autofocus: true, + }); +}; diff --git a/devtools/client/webconsole/package.json b/devtools/client/webconsole/package.json new file mode 100644 index 0000000000..6349f2057c --- /dev/null +++ b/devtools/client/webconsole/package.json @@ -0,0 +1,20 @@ +{ + "name": "webconsole", + "version": "0.0.1", + "devDependencies": { + "amd-loader": "0.0.5", + "babel-preset-es2015": "^6.6.0", + "babel-register": "^6.7.2", + "enzyme": "^2.4.1", + "expect": "^1.16.0", + "jsdom": "^9.4.1", + "jsdom-global": "^2.0.0", + "mocha": "^2.5.3", + "require-hacker": "^2.1.4", + "sinon": "^1.17.5" + }, + "scripts": { + "postinstall": "cd ../ && npm install && cd webconsole", + "test": "NODE_PATH=`pwd`/../../../:`pwd`/../../../devtools/client/shared/vendor/ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/requireHelper.js" + } +} diff --git a/devtools/client/webconsole/panel.js b/devtools/client/webconsole/panel.js new file mode 100644 index 0000000000..3e3a4f4b98 --- /dev/null +++ b/devtools/client/webconsole/panel.js @@ -0,0 +1,118 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const promise = require("promise"); + +loader.lazyGetter(this, "HUDService", () => require("devtools/client/webconsole/hudservice")); +loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter")); + +/** + * A DevToolPanel that controls the Web Console. + */ +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 () { + this.hud.jsterm.focus(); + }, + + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the Web Console completes opening. + */ + open: function () { + let parentDoc = this._toolbox.doc; + let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole"); + + // Make sure the iframe content window is ready. + let deferredIframe = promise.defer(); + let win, doc; + if ((win = iframe.contentWindow) && + (doc = win.document) && + doc.readyState == "complete") { + deferredIframe.resolve(null); + } else { + iframe.addEventListener("load", function onIframeLoad() { + iframe.removeEventListener("load", onIframeLoad, true); + deferredIframe.resolve(null); + }, true); + } + + // Local debugging needs to make the target remote. + let promiseTarget; + if (!this.target.isRemote) { + promiseTarget = this.target.makeRemote(); + } else { + promiseTarget = promise.resolve(this.target); + } + + // 1. Wait for the iframe to load. + // 2. Wait for the remote target. + // 3. Open the Web Console. + return deferredIframe.promise + .then(() => promiseTarget) + .then((target) => { + this._frameWindow._remoteTarget = target; + + let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject; + let chromeWindow = iframe.ownerDocument.defaultView; + return HUDService.openWebConsole(this.target, webConsoleUIWindow, + chromeWindow); + }) + .then((webConsole) => { + this.hud = webConsole; + this._isReady = true; + this.emit("ready"); + return this; + }, (reason) => { + let msg = "WebConsolePanel open failed. " + + reason.error + ": " + reason.message; + dump(msg + "\n"); + console.error(msg); + }); + }, + + get target() { + return this._toolbox.target; + }, + + _isReady: false, + get isReady() { + return this._isReady; + }, + + destroy: function () { + if (this._destroyer) { + return this._destroyer; + } + + this._destroyer = this.hud.destroy(); + this._destroyer.then(() => { + this._frameWindow = null; + this._toolbox = null; + this.emit("destroyed"); + }); + + return this._destroyer; + }, +}; diff --git a/devtools/client/webconsole/test/.eslintrc.js b/devtools/client/webconsole/test/.eslintrc.js new file mode 100644 index 0000000000..8d15a76d9b --- /dev/null +++ b/devtools/client/webconsole/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/webconsole/test/browser.ini b/devtools/client/webconsole/test/browser.ini new file mode 100644 index 0000000000..918411182f --- /dev/null +++ b/devtools/client/webconsole/test/browser.ini @@ -0,0 +1,396 @@ +[DEFAULT] +tags = devtools +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-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-clear.html + test-console-count.html + test-console-count-external-file.js + test-console-extras.html + test-console-replaced-api.html + test-console-server-logging.sjs + test-console-server-logging-array.sjs + test-console.html + test-console-workers.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-output-regexp.html + test-console-column.html + test-consoleiframes.html + test-console-trace-async.html + test-certificate-messages.html + test-cu-reporterror.js + 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_hpkp-invalid-headers.sjs + test_hsts-invalid-headers.sjs + 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-trackingprotection-securityerrors.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-bug_1050691_click_function_to_source.html + test-bug_1050691_click_function_to_source.js + test-console-api-stackframe.html + test-exception-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 + test_bug_1247459_violation.html + !/devtools/client/framework/test/shared-head.js + !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs + !/image/test/mochitest/blue.png + +[browser_bug1045902_console_csp_ignore_reflected_xss_message.js] +skip-if = (e10s && debug) || (e10s && os == 'win') # Bug 1221499 enabled these on windows +[browser_bug664688_sandbox_update_after_navigation.js] +[browser_bug_638949_copy_link_location.js] +subsuite = clipboard +[browser_bug_862916_console_dir_and_filter_off.js] +skip-if = (e10s && (os == 'win' || os == 'mac')) # Bug 1243976 +[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] +[browser_cached_messages.js] +[browser_console.js] +[browser_console_addonsdk_loader_exception.js] +[browser_console_clear_method.js] +[browser_console_clear_on_reload.js] +[browser_console_click_focus.js] +[browser_console_consolejsm_output.js] +[browser_console_copy_command.js] +subsuite = clipboard +[browser_console_dead_objects.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_copy_entire_message_context_menu.js] +subsuite = clipboard +[browser_console_error_source_click.js] +[browser_console_filters.js] +[browser_console_iframe_messages.js] +[browser_console_keyboard_accessibility.js] +[browser_console_log_inspectable_object.js] +[browser_console_native_getters.js] +[browser_console_navigation_marker.js] +[browser_console_netlogging.js] +[browser_console_nsiconsolemessage.js] +[browser_console_optimized_out_vars.js] +[browser_console_private_browsing.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests +[browser_console_server_logging.js] +[browser_console_variables_view.js] +[browser_console_variables_view_filter.js] +[browser_console_variables_view_dom_nodes.js] +[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js] +[browser_console_variables_view_special_names.js] +[browser_console_variables_view_while_debugging.js] +[browser_console_variables_view_while_debugging_and_inspecting.js] +[browser_eval_in_debugger_stackframe.js] +[browser_eval_in_debugger_stackframe2.js] +[browser_jsterm_inspect.js] +skip-if = e10s && debug && (os == 'win' || os == 'mac') # Bug 1243966 +[browser_longstring_hang.js] +[browser_output_breaks_after_console_dir_uninspectable.js] +[browser_output_longstring_expand.js] +[browser_repeated_messages_accuracy.js] +[browser_result_format_as_string.js] +[browser_warn_user_about_replaced_api.js] +[browser_webconsole_allow_mixedcontent_securityerrors.js] +tags = mcb +[browser_webconsole_script_errordoc_urls.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_assert.js] +[browser_webconsole_block_mixedcontent_securityerrors.js] +tags = mcb +[browser_webconsole_bug_579412_input_focus.js] +[browser_webconsole_bug_580001_closing_after_completion.js] +[browser_webconsole_bug_580030_errors_after_page_reload.js] +[browser_webconsole_bug_580454_timestamp_l10n.js] +[browser_webconsole_bug_582201_duplicate_errors.js] +[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] +[browser_webconsole_bug_585991_autocomplete_popup.js] +[browser_webconsole_bug_586388_select_all.js] +[browser_webconsole_bug_587617_output_copy.js] +subsuite = clipboard +[browser_webconsole_bug_588342_document_focus.js] +[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] +[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 +[browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js] +[browser_webconsole_bug_597136_external_script_errors.js] +[browser_webconsole_bug_597136_network_requests_from_chrome.js] +[browser_webconsole_bug_597460_filter_scroll.js] +[browser_webconsole_bug_597756_reopen_closed_tab.js] +[browser_webconsole_bug_599725_response_headers.js] +[browser_webconsole_bug_600183_charset.js] +[browser_webconsole_bug_601177_log_levels.js] +[browser_webconsole_bug_601352_scroll.js] +[browser_webconsole_bug_601667_filter_buttons.js] +[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] +subsuite = clipboard +[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] +[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 = true # Bug 1244707 +[browser_webconsole_bug_642108_pruneTest.js] +[browser_webconsole_autocomplete_and_selfxss.js] +subsuite = clipboard +[browser_webconsole_bug_644419_log_limits.js] +[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 = true # Requires direct access to content nodes +[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] +tags = mcb +[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js] +[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js] +[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] +[browser_webconsole_bug_770099_violation.js] +skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243978 +[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js] +[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] +[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] +skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243967 +[browser_webconsole_bug_1247459_violation.js] +skip-if = e10s && (os == 'win') # Bug 1264955 +[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 && (os == 'win' || os == 'mac') # Bug 1243987 +[browser_webconsole_cached_autocomplete.js] +[browser_webconsole_chrome.js] +[browser_webconsole_clear_method.js] +[browser_webconsole_clickable_urls.js] +[browser_webconsole_closure_inspection.js] +[browser_webconsole_completion.js] +[browser_webconsole_console_extras.js] +[browser_webconsole_console_logging_api.js] +[browser_webconsole_console_logging_workers_api.js] +[browser_webconsole_console_trace_async.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_hpkp_invalid-headers.js] +[browser_webconsole_hsts_invalid-headers.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests +[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_multiline_input.js] +[browser_webconsole_netlogging.js] +skip-if = true # Bug 1298364 +[browser_webconsole_netlogging_basic.js] +[browser_webconsole_netlogging_panel.js] +[browser_webconsole_netlogging_reset_filter.js] +[browser_webconsole_notifications.js] +[browser_webconsole_open-links-without-callback.js] +[browser_webconsole_promise.js] +[browser_webconsole_output_copy_newlines.js] +subsuite = clipboard +[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] +[browser_webconsole_trackingprotection_errors.js] +tags = trackingprotection +[browser_webconsole_view_source.js] +[browser_webconsole_reflow.js] +[browser_webconsole_log_file_filter.js] +[browser_webconsole_expandable_timestamps.js] +[browser_webconsole_autocomplete_accessibility.js] +[browser_webconsole_autocomplete_in_debugger_stackframe.js] +[browser_webconsole_autocomplete_popup_close_on_tab_switch.js] +[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js] +[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js] +[browser_console_history_persist.js] +[browser_webconsole_output_01.js] +[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] +skip-if = e10s # Bug 1241019 +[browser_webconsole_output_dom_elements_04.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_output_dom_elements_05.js] +[browser_webconsole_output_events.js] +[browser_webconsole_output_regexp.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_exception_stackframe.js] +[browser_webconsole_column_numbers.js] +[browser_console_open_or_focus.js] +[browser_webconsole_bug_922212_console_dirxml.js] +[browser_webconsole_shows_reqs_in_netmonitor.js] +[browser_netmonitor_shows_reqs_in_webconsole.js] +[browser_webconsole_bug_1050691_click_function_to_source.js] +[browser_webconsole_context_menu_open_in_var_view.js] +[browser_webconsole_context_menu_store_as_global.js] +[browser_webconsole_strict_mode_errors.js] diff --git a/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js new file mode 100644 index 0000000000..cfbf617959 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 a file with an unsupported CSP directive ('reflected-xss filter') +// displays the appropriate message to the console. + +"use strict"; + +const EXPECTED_RESULT = "Not supporting directive \u2018reflected-xss\u2019. " + + "Directive and values will be ignored."; +const TEST_FILE = "http://example.com/browser/devtools/client/webconsole/" + + "test/test_bug1045902_console_csp_ignore_reflected_xss_" + + "message.html"; + +var hud = undefined; + +var TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring " + + "reflected XSS (bug 1045902)"; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + hud = yield openConsole(); + + yield loadDocument(browser); + yield testViolationMessage(); + + hud = null; +}); + +function loadDocument(browser) { + hud.jsterm.clearOutput(); + browser.loadURI(TEST_FILE); + return BrowserTestUtils.browserLoaded(browser); +} + +function testViolationMessage() { + let aOutputNode = hud.outputNode; + + return waitForSuccess({ + name: "Confirming that CSP logs messages to the console when " + + "\u2018reflected-xss\u2019 directive is used!", + validator: function () { + console.log(aOutputNode.textContent); + let success = false; + success = aOutputNode.textContent.indexOf(EXPECTED_RESULT) > -1; + return success; + } + }); +} diff --git a/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js new file mode 100644 index 0000000000..1aacb61c14 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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"; + +add_task(function* () { + const TEST_URI1 = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + const TEST_URI2 = "http://example.org/browser/devtools/client/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 + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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"); + + // Navigation clears messages. Wait for that clear to happen before + // continuing the test or it might destroy messages we wait later on (Bug + // 1270234). + let cleared = hud.jsterm.once("messages-cleared"); + + gBrowser.goBack(); + + info("Waiting for messages to be cleared due to navigation"); + yield cleared; + + info("Messages cleared after navigation; checking location"); + 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/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js new file mode 100644 index 0000000000..54bdbe4998 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js @@ -0,0 +1,107 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 for the "Copy link location" context menu item shown when you right +// click network requests in the output. + +"use strict"; + +add_task(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/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. + yield waitForContextMenu(menu, message, () => { + let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden; + ok(isHidden, CONTEXT_MENU_ID + " is hidden"); + }); + + hud.jsterm.clearOutput(); + // Reloading will produce network logging + content.location.reload(); + + // 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. + 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/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js new file mode 100644 index 0000000000..9d04076ee9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js @@ -0,0 +1,31 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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"; + +add_task(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/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js new file mode 100644 index 0000000000..86ab5bd392 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/" + + "test/test-repeated-messages.html"; + +add_task(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/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js new file mode 100644 index 0000000000..044525b28f --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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/devtools/client/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 result; + let vview; + let msg; + + yield openSidebar("fooObj", + 'testProp: "testValue"', + { name: "testProp", value: "testValue" }); + + let prop = result.matchedProp; + ok(prop, "matched the |testProp| property in the variables view"); + + vview.window.focus(); + + let sidebarClosed = jsterm.once("sidebar-closed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield sidebarClosed; + + jsterm.clearOutput(); + + yield openSidebar("window.location", + "Location \u2192 http://example.com/browser/", + { name: "host", value: "example.com" }); + + vview.window.focus(); + + msg.scrollIntoView(); + sidebarClosed = jsterm.once("sidebar-closed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield sidebarClosed; + + function* openSidebar(objName, expectedText, expectedObj) { + msg = yield jsterm.execute(objName); + 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.includes(expectedText), "message text check"); + + msg.scrollIntoView(); + yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow); + + let vviewVar = yield jsterm.once("variablesview-fetched"); + vview = vviewVar._variablesView; + ok(vview, "variables view object exists"); + + [result] = yield findVariableViewProperties(vviewVar, [ + expectedObj, + ], { webconsole: hud }); + } + } +} diff --git a/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js new file mode 100644 index 0000000000..685148fc71 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that users can inspect objects logged from cross-domain iframes - +// bug 869003. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-869003-top-window.html"; + +add_task(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(); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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.includes('{ 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/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js new file mode 100644 index 0000000000..c1698cf919 --- /dev/null +++ b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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"; + +add_task(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/devtools/client/webconsole/test/browser_cached_messages.js b/devtools/client/webconsole/test/browser_cached_messages.js new file mode 100644 index 0000000000..bf69deee3f --- /dev/null +++ b/devtools/client/webconsole/test/browser_cached_messages.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 to see if the cached messages are displayed when the console UI is +// opened. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-webconsole-error-observer.html"; + +// On e10s, the exception is triggered in child process +// and is ignored by test harness +if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); +} + +function test() { + waitForExplicitFinish(); + + 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/devtools/client/webconsole/test/browser_console.js b/devtools/client/webconsole/test/browser_console.js new file mode 100644 index 0000000000..7bd1ffdc23 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console.js @@ -0,0 +1,160 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the basic features of the Browser Console, bug 587757. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html?" + Date.now(); +const TEST_FILE = "chrome://mochitests/content/browser/devtools/client/" + + "webconsole/test/test-cu-reporterror.js"; + +const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`; + +const TEST_IMAGE = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-image.png"; + +"use strict"; + +add_task(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"); + + // Check Cu.reportError stack. + // Use another js script to not depend on the test file line numbers. + Services.scriptloader.loadSubScript(TEST_FILE, hud.iframeWindow); + + // 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(); + + // Check that Fetch requests are categorized as "XHR". + fetch(TEST_IMAGE).then(() => { console.log("fetch loaded"); }); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + name: "chrome window console.log() is displayed", + text: "bug587757a", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "Cu.reportError is displayed", + text: "bug1141222", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + stacktrace: [{ + file: TEST_FILE, + line: 2, + }, { + file: TEST_FILE, + line: 4, + }, + // Ignore the rest of the stack, + // just assert Cu.reportError call site + // and consoleOpened call + ] + }, + { + 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, + }, + { + name: "network message", + text: "test-image.png", + category: CATEGORY_NETWORK, + severity: SEVERITY_INFO, + 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/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js new file mode 100644 index 0000000000..3eec65de35 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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; + // non-iterable + toolbox._toolPanels = {}; + + 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 .message-location > .frame-link"); + ok(locationNode, "message location element found"); + + let url = locationNode.getAttribute("data-url"); + info("location node url: " + url); + ok(url.indexOf("resource://") === 0, "error comes from a subscript"); + + let viewSource = browserconsole.viewSource; + let URL = null; + let clickPromise = promise.defer(); + browserconsole.viewSourceInDebugger = (sourceURL) => { + info("browserconsole.viewSourceInDebugger() was invoked: " + sourceURL); + URL = sourceURL; + clickPromise.resolve(null); + }; + + msg.scrollIntoView(); + EventUtils.synthesizeMouse(locationNode, 2, 2, {}, + browserconsole.iframeWindow); + + info("wait for click on locationNode"); + yield clickPromise.promise; + + 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.viewSourceInDebugger = viewSource; + } +} diff --git a/devtools/client/webconsole/test/browser_console_clear_method.js b/devtools/client/webconsole/test/browser_console_clear_method.js new file mode 100644 index 0000000000..33b43850e4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_clear_method.js @@ -0,0 +1,41 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that console.clear() does not clear the output of the browser console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>Bug 1296870"; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield HUDService.toggleBrowserConsole(); + + info("Log a new message from the content page"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.log("msg"); + }); + yield waitForMessage("msg", hud); + + info("Send a console.clear() from the content page"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.clear(); + }); + yield waitForMessage("Console was cleared", hud); + + info("Check that the messages logged after the first clear are still displayed"); + isnot(hud.outputNode.textContent.indexOf("msg"), -1, "msg is in the output"); +}); + +function waitForMessage(message, webconsole) { + return waitForMessages({ + webconsole, + messages: [{ + text: message, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} diff --git a/devtools/client/webconsole/test/browser_console_clear_on_reload.js b/devtools/client/webconsole/test/browser_console_clear_on_reload.js new file mode 100644 index 0000000000..223eb028d9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_clear_on_reload.js @@ -0,0 +1,86 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that clear output on page reload works - bug 705921. +// Check that clear output and page reload remove the sidebar - bug 971967. + +"use strict"; + +add_task(function* () { + const PREF = "devtools.webconsole.persistlog"; + const TEST_URI = "http://example.com/browser/devtools/client/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"); + + yield openSidebar("fooObj", { name: "testProp", value: "testValue" }); + + let sidebarClosed = hud.jsterm.once("sidebar-closed"); + hud.jsterm.clearOutput(); + yield sidebarClosed; + + hud.jsterm.execute("console.log('foobarz1')"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + yield openSidebar("fooObj", { name: "testProp", value: "testValue" }); + + BrowserReload(); + + sidebarClosed = hud.jsterm.once("sidebar-closed"); + loadBrowser(gBrowser.selectedBrowser); + yield sidebarClosed; + + 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"); + + function* openSidebar(objName, expectedObj) { + let msg = yield hud.jsterm.execute(objName); + ok(msg, "output message found"); + + let anchor = msg.querySelector("a"); + let body = msg.querySelector(".message-body"); + ok(anchor, "object anchor"); + ok(body, "message body"); + + yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow); + + let vviewVar = yield hud.jsterm.once("variablesview-fetched"); + let vview = vviewVar._variablesView; + ok(vview, "variables view object exists"); + + yield findVariableViewProperties(vviewVar, [ + expectedObj, + ], { webconsole: hud }); + } +}); diff --git a/devtools/client/webconsole/test/browser_console_click_focus.js b/devtools/client/webconsole/test/browser_console_click_focus.js new file mode 100644 index 0000000000..f405f0bbf3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_click_focus.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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"); + + yield waitForBlurredInput(inputNode); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + ok(inputNode.getAttribute("focused"), "input node is focused, second time"); + + yield waitForBlurredInput(inputNode); + + info("Setting a text selection and making sure a click does not re-focus"); + let selection = hud.iframeWindow.getSelection(); + selection.selectAllChildren(outputItem); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + ok(!inputNode.getAttribute("focused"), + "input node is not focused after drag"); +}); + +function waitForBlurredInput(inputNode) { + return new Promise(resolve => { + let lostFocus = () => { + inputNode.removeEventListener("blur", lostFocus); + ok(!inputNode.getAttribute("focused"), "input node is not focused"); + resolve(); + }; + inputNode.addEventListener("blur", lostFocus); + document.getElementById("urlbar").click(); + }); +} diff --git a/devtools/client/webconsole/test/browser_console_consolejsm_output.js b/devtools/client/webconsole/test/browser_console_consolejsm_output.js new file mode 100644 index 0000000000..e5b37843e5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_consolejsm_output.js @@ -0,0 +1,285 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 Console.jsm outputs messages to the Browser Console, bug 851231. + +"use strict"; + +function onNewMessage(aEvent, aNewMessages) { + for (let msg of aNewMessages) { + // Messages that shouldn't be output contain the substring FAIL_TEST + if (msg.node.textContent.includes("FAIL_TEST")) { + ok(false, "Message shouldn't have been output: " + msg.node.textContent); + } + } +} + +add_task(function* () { + let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + let {console} = Cu.import("resource://gre/modules/Console.jsm", {}); + 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 }); + + yield HUDService.toggleBrowserConsole(); +}); + +add_task(function* testPrefix() { + let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {}); + let consoleOptions = { + maxLogLevel: "error", + prefix: "Log Prefix", + }; + let console2 = new ConsoleAPI(consoleOptions); + console2.error("Testing a prefix"); + console2.log("FAIL_TEST: Below the maxLogLevel"); + + let hud = yield HUDService.toggleBrowserConsole(); + hud.ui.on("new-messages", onNewMessage); + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "cached console.error message", + prefix: "Log Prefix:", + severity: SEVERITY_ERROR, + text: "Testing a prefix", + }], + }); + + hud.jsterm.clearOutput(true); + hud.ui.off("new-messages", onNewMessage); + yield HUDService.toggleBrowserConsole(); +}); + +add_task(function* testMaxLogLevelPrefMissing() { + let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {}); + let consoleOptions = { + maxLogLevel: "error", + maxLogLevelPref: "testing.maxLogLevel", + }; + let console = new ConsoleAPI(consoleOptions); + + is(Services.prefs.getPrefType(consoleOptions.maxLogLevelPref), + Services.prefs.PREF_INVALID, + "Check log level pref is missing"); + + // Since the maxLogLevelPref doesn't exist, we should fallback to the passed + // maxLogLevel of "error". + console.warn("FAIL_TEST: Below the maxLogLevel"); + console.error("Error should be shown"); + + let hud = yield HUDService.toggleBrowserConsole(); + + hud.ui.on("new-messages", onNewMessage); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "defaulting to error level", + severity: SEVERITY_ERROR, + text: "Error should be shown", + }], + }); + + hud.jsterm.clearOutput(true); + hud.ui.off("new-messages", onNewMessage); + yield HUDService.toggleBrowserConsole(); +}); + +add_task(function* testMaxLogLevelPref() { + let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {}); + let consoleOptions = { + maxLogLevel: "error", + maxLogLevelPref: "testing.maxLogLevel", + }; + + info("Setting the pref to warn"); + Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "Warn"); + + let console = new ConsoleAPI(consoleOptions); + + is(console.maxLogLevel, "warn", "Check pref was read at initialization"); + + console.info("FAIL_TEST: info is below the maxLogLevel"); + console.error("Error should be shown"); + console.warn("Warn should be shown due to the initial pref value"); + + info("Setting the pref to info"); + Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "INFO"); + is(console.maxLogLevel, "info", "Check pref was lowercased"); + + console.info("info should be shown due to the pref change being observed"); + + info("Clearing the pref"); + Services.prefs.clearUserPref(consoleOptions.maxLogLevelPref); + + console.warn("FAIL_TEST: Shouldn't be shown due to defaulting to error"); + console.error("Should be shown due to defaulting to error"); + + let hud = yield HUDService.toggleBrowserConsole(); + hud.ui.on("new-messages", onNewMessage); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "error > warn", + severity: SEVERITY_ERROR, + text: "Error should be shown", + }, + { + name: "warn is the inital pref value", + severity: SEVERITY_WARNING, + text: "Warn should be shown due to the initial pref value", + }, + { + name: "pref changed to info", + severity: SEVERITY_INFO, + text: "info should be shown due to the pref change being observed", + }, + { + name: "default to intial maxLogLevel if pref is removed", + severity: SEVERITY_ERROR, + text: "Should be shown due to defaulting to error", + }], + }); + + hud.jsterm.clearOutput(true); + hud.ui.off("new-messages", onNewMessage); + yield HUDService.toggleBrowserConsole(); +}); diff --git a/devtools/client/webconsole/test/browser_console_copy_command.js b/devtools/client/webconsole/test/browser_console_copy_command.js new file mode 100644 index 0000000000..c4ed4360f4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_copy_command.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the `copy` console helper works as intended. + +"use strict"; + +var gWebConsole, gJSTerm; + +var 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(); + +var 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* testCopy() { + 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/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js new file mode 100644 index 0000000000..bdd4f71793 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js @@ -0,0 +1,97 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +/* globals goDoCommand */ + +"use strict"; + +// Test copying of the entire console message when right-clicked +// with no other text selected. See Bug 1100562. + +add_task(function* () { + let hud; + let outputNode; + let contextMenu; + + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console.html"; + + const { tab, browser } = 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(); + + yield ContentTask.spawn(browser, {}, function* () { + let button = content.document.getElementById("testTrace"); + button.click(); + }); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "bug 1100562", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + lines: 1, + }, + { + name: "console.trace output", + consoleTrace: true, + lines: 3, + }, + ] + }); + + outputNode.focus(); + + for (let result of results) { + let message = [...result.matched][0]; + + yield waitForContextMenu(contextMenu, message, () => { + 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"); + }); + + let clipboardText; + + yield waitForClipboardPromise( + () => goDoCommand("cmd_copy"), + (str) => { + clipboardText = str; + return message.textContent == clipboardText; + } + ); + + ok(clipboardText, "Clipboard text was found and saved"); + + let lines = clipboardText.split("\n"); + ok(lines.length > 0, "There is at least one newline in the message"); + is(lines.pop(), "", "There is a newline at the end"); + is(lines.length, result.lines, `There are ${result.lines} lines in the message`); + + // Test the first line for "timestamp message repeat file:line" + let firstLine = lines.shift(); + ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine), + "The message's first line has the right format"); + + // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col" + for (let line of lines) { + ok(/^\t.+ .+:\d+:\d+$/.test(line), "The stack trace line has the right format"); + } + } + + yield closeConsole(tab); + yield finishTest(); +}); diff --git a/devtools/client/webconsole/test/browser_console_dead_objects.js b/devtools/client/webconsole/test/browser_console_dead_objects.js new file mode 100644 index 0000000000..46b15d59bd --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_dead_objects.js @@ -0,0 +1,88 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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); + 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/devtools/client/webconsole/test/browser_console_error_source_click.js b/devtools/client/webconsole/test/browser_console_error_source_click.js new file mode 100644 index 0000000000..5839f20d51 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_error_source_click.js @@ -0,0 +1,79 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that JS errors and CSS warnings open view source when their source link +// is clicked in the Browser Console. See bug 877778. + +"use strict"; + +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>"; + +add_task(function* () { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["devtools.browserconsole.filter.cssparser", true] + ]}, resolve); + }); + + yield loadTab(TEST_URI); + let hud = yield HUDService.toggleBrowserConsole(); + ok(hud, "browser console opened"); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + info("generate exception and wait for the message"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let button = content.document.querySelector("button"); + button.click(); + }); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "ReferenceError: foobar is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + { + text: "Unknown property \u2018test-color\u2019", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + ], + }); + + let viewSourceCalled = false; + + let viewSource = hud.viewSource; + hud.viewSource = () => { + viewSourceCalled = true; + }; + + for (let result of results) { + viewSourceCalled = false; + + let msg = [...result.matched][0]; + ok(msg, "message element found for: " + result.text); + ok(!msg.classList.contains("filtered-by-type"), "message element is not filtered"); + let selector = ".message .message-location .frame-link-source"; + let locationNode = msg.querySelector(selector); + ok(locationNode, "message location element found"); + + locationNode.click(); + + ok(viewSourceCalled, "view source opened"); + } + + hud.viewSource = viewSource; + + yield finishTest(); +}); diff --git a/devtools/client/webconsole/test/browser_console_filters.js b/devtools/client/webconsole/test/browser_console_filters.js new file mode 100644 index 0000000000..072766fdb4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_filters.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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."; + +add_task(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/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js new file mode 100644 index 0000000000..d3fdb08be8 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js @@ -0,0 +1,114 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +/* + * 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. + */ + +"use strict"; + +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(); + + // Filter out other messages to ensure ours stays visible. + hud.ui.filterBox.value = "browser_console_hide_jsterm_test"; + + hud.jsterm.clearOutput(); + hud.jsterm.execute("new Object({ browser_console_hide_jsterm_test: true })"); + + let [message] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Object { browser_console_hide_jsterm_test: true }", + 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/devtools/client/webconsole/test/browser_console_history_persist.js b/devtools/client/webconsole/test/browser_console_history_persist.js new file mode 100644 index 0000000000..61c4cbf4df --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_history_persist.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 console command input is persisted across toolbox loads. +// See Bug 943306. + +"use strict"; + +requestLongerTimeout(2); + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "persisting history - bug 943306"; +const INPUT_HISTORY_COUNT = 10; + +add_task(function* () { + info("Setting custom input history pref to " + INPUT_HISTORY_COUNT); + Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount", + INPUT_HISTORY_COUNT); + + // First tab: run a bunch of commands and then make sure that you can + // navigate through their history. + yield loadTab(TEST_URI); + let hud1 = yield openConsole(); + is(JSON.stringify(hud1.jsterm.history), "[]", + "No history on first tab initially"); + yield populateInputHistory(hud1); + is(JSON.stringify(hud1.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9"]', + "First tab has populated history"); + + // Second tab: Just make sure that you can navigate through the history + // generated by the first tab. + yield loadTab(TEST_URI); + let hud2 = yield openConsole(); + is(JSON.stringify(hud2.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9"]', + "Second tab has populated history"); + yield testNaviatingHistoryInUI(hud2); + is(JSON.stringify(hud2.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9",""]', + "An empty entry has been added in the second tab due to history perusal"); + + // Third tab: Should have the same history as first tab, but if we run a + // command, then the history of the first and second shouldn't be affected + yield loadTab(TEST_URI); + let hud3 = yield openConsole(); + is(JSON.stringify(hud3.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9"]', + "Third tab has populated history"); + + // Set input value separately from execute so UP arrow accurately navigates + // history. + hud3.jsterm.setInputValue('"hello from third tab"'); + hud3.jsterm.execute(); + + is(JSON.stringify(hud1.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9"]', + "First tab history hasn't changed due to command in third tab"); + is(JSON.stringify(hud2.jsterm.history), + '["0","1","2","3","4","5","6","7","8","9",""]', + "Second tab history hasn't changed due to command in third tab"); + is(JSON.stringify(hud3.jsterm.history), + '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Third tab has updated history (and purged the first result) after " + + "running a command"); + + // Fourth tab: Should have the latest command from the third tab, followed + // by the rest of the history from the first tab. + yield loadTab(TEST_URI); + let hud4 = yield openConsole(); + is(JSON.stringify(hud4.jsterm.history), + '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Fourth tab has most recent history"); + + yield hud4.jsterm.clearHistory(); + is(JSON.stringify(hud4.jsterm.history), "[]", + "Clearing history for a tab works"); + + yield loadTab(TEST_URI); + let hud5 = yield openConsole(); + is(JSON.stringify(hud5.jsterm.history), "[]", + "Clearing history carries over to a new tab"); + + info("Clearing custom input history pref"); + Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount"); +}); + +/** + * Populate the history by running the following commands: + * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + */ +function* populateInputHistory(hud) { + let jsterm = hud.jsterm; + + for (let i = 0; i < INPUT_HISTORY_COUNT; i++) { + // Set input value separately from execute so UP arrow accurately navigates + // history. + jsterm.setInputValue(i); + jsterm.execute(); + } +} + +/** + * Check pressing up results in history traversal like: + * [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + */ +function* testNaviatingHistoryInUI(hud) { + let jsterm = hud.jsterm; + jsterm.focus(); + + // Count backwards from original input and make sure that pressing up + // restores this. + for (let i = INPUT_HISTORY_COUNT - 1; i >= 0; i--) { + EventUtils.synthesizeKey("VK_UP", {}); + is(jsterm.getInputValue(), i, "Pressing up restores last input"); + } +} diff --git a/devtools/client/webconsole/test/browser_console_iframe_messages.js b/devtools/client/webconsole/test/browser_console_iframe_messages.js new file mode 100644 index 0000000000..9bf3fe2b7c --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_iframe_messages.js @@ -0,0 +1,114 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that cached messages from nested iframes are displayed in the +// Web/Browser Console. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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 + }, +]; + +add_task(function* () { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + yield loadTab(TEST_URI); + let hud = yield openConsole(); + ok(hud, "web console opened"); + + yield testWebConsole(hud); + yield closeConsole(); + info("web console closed"); + + hud = yield HUDService.toggleBrowserConsole(); + yield testBrowserConsole(hud); + yield closeConsole(); +}); + +function* testWebConsole(hud) { + yield waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }); + + info("first messages matched"); + + yield waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }); +} + +function* testBrowserConsole(hud) { + ok(hud, "browser console opened"); + + // TODO: The browser console doesn't show page's console.log statements + // in e10s windows. See Bug 1241289. + if (Services.appinfo.browserTabsRemoteAutostart) { + todo(false, "Bug 1241289"); + return; + } + + yield waitForMessages({ + webconsole: hud, + messages: expectedMessages, + }); + + info("first messages matched"); + yield waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }); +} diff --git a/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js new file mode 100644 index 0000000000..c64e45f5d7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js @@ -0,0 +1,89 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that basic keyboard shortcuts work in the web console. + +"use strict"; + +add_task(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/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.ui.outputWrapper.scrollTop; + let bottom = currentPosition; + + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + isnot(hud.ui.outputWrapper.scrollTop, currentPosition, + "scroll position changed after page up"); + + currentPosition = hud.ui.outputWrapper.scrollTop; + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + ok(hud.ui.outputWrapper.scrollTop > currentPosition, + "scroll position now at bottom"); + + EventUtils.synthesizeKey("VK_HOME", {}); + is(hud.ui.outputWrapper.scrollTop, 0, "scroll position now at top"); + + EventUtils.synthesizeKey("VK_END", {}); + + let scrollTop = hud.ui.outputWrapper.scrollTop; + ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5, + "scroll position now at bottom"); + + info("try ctrl-l to clear output"); + executeSoon(() => { + let clearShortcut; + if (Services.appinfo.OS === "Darwin") { + clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX"); + } else { + clearShortcut = WCUL10n.getStr("webconsole.clear.key"); + } + synthesizeKeyShortcut(clearShortcut); + }); + 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"); + synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key")); + 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/devtools/client/webconsole/test/browser_console_log_inspectable_object.js b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js new file mode 100644 index 0000000000..f9fd85295a --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 objects given to console.log() are inspectable. + +"use strict"; + +add_task(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); + + yield 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.includes('{ 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/devtools/client/webconsole/test/browser_console_native_getters.js b/devtools/client/webconsole/test/browser_console_native_getters.js new file mode 100644 index 0000000000..1afb707965 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_native_getters.js @@ -0,0 +1,101 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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!"; + +requestLongerTimeout(2); + +add_task(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: "Array [", + 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/devtools/client/webconsole/test/browser_console_navigation_marker.js b/devtools/client/webconsole/test/browser_console_navigation_marker.js new file mode 100644 index 0000000000..e8ec84caf3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_navigation_marker.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that the navigation marker shows on page reload - bug 793996. + +"use strict"; + +const PREF = "devtools.webconsole.persistlog"; +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +var hud; + +add_task(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(); + + ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + content.console.log("foobarz1"); + }); + + return waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +function onReload() { + ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + 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/devtools/client/webconsole/test/browser_console_netlogging.js b/devtools/client/webconsole/test/browser_console_netlogging.js new file mode 100644 index 0000000000..a6f7bec48b --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_netlogging.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 network log messages bring up the network panel. + +"use strict"; + +const TEST_NETWORK_REQUEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/" + + "test-network-request.html"; + +add_task(function* () { + let finishedRequest = waitForFinishedRequest(({ request }) => { + return request.url === TEST_NETWORK_REQUEST_URI; + }); + + const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI, + "browserConsole"); + let request = yield finishedRequest; + + ok(request, "Page load was logged"); + + let client = hud.ui.webConsoleClient; + let args = [request.actor]; + const postData = yield getPacket(client, "getRequestPostData", args); + const responseContent = yield getPacket(client, "getResponseContent", args); + + is(request.request.url, TEST_NETWORK_REQUEST_URI, + "Logged network entry is page load"); + is(request.request.method, "GET", "Method is correct"); + ok(!postData.postData.text, "No request body was stored"); + ok(postData.postDataDiscarded, "Request body was discarded"); + ok(!responseContent.content.text, "No response body was stored"); + ok(responseContent.contentDiscarded || request.fromCache, + "Response body was discarded or response came from the cache"); +}); diff --git a/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js new file mode 100644 index 0000000000..fedd0c71c3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js @@ -0,0 +1,85 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that nsIConsoleMessages are displayed in the Browser Console. +// See bug 859756. + +"use strict"; + +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"); + + ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + 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/devtools/client/webconsole/test/browser_console_open_or_focus.js b/devtools/client/webconsole/test/browser_console_open_or_focus.js new file mode 100644 index 0000000000..d537c9aadb --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_open_or_focus.js @@ -0,0 +1,46 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the "browser console" menu item opens or focuses (if already open) +// the console window instead of toggling it open/close. + +"use strict"; + +var {Tools} = require("devtools/client/definitions"); + +add_task(function* () { + 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, 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, 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/devtools/client/webconsole/test/browser_console_optimized_out_vars.js b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js new file mode 100644 index 0000000000..dc898eb2b8 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that inspecting an optimized out variable works when execution is +// paused. + +"use strict"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +function test() { + Task.spawn(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-closure-optimized-out.html"; + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let { toolbox, panel, panelWin } = yield openDebugger(); + + 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); + + // Cause the debuggee to pause + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let button = content.document.querySelector("button"); + button.click(); + }); + + 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 devtools/client/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); + } + 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(eventName, ...args) { + info("Thread event '" + eventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + eventName + "' thread events have been fired."); + thread.removeListener(eventName, onEvent); + deferred.resolve.apply(deferred, args); + } + }); + + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_console_private_browsing.js b/devtools/client/webconsole/test/browser_console_private_browsing.js new file mode 100644 index 0000000000..4b3a793297 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_private_browsing.js @@ -0,0 +1,192 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Bug 874061: test for how the browser and web consoles display messages coming +// from private windows. See bug for description of expected behavior. + +"use strict"; + +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. + ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + 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's 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(injectedHud) { + hud = injectedHud; + 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(injectedHud) { + hud = injectedHud; + 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"); + 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(injectedHud) { + hud = injectedHud; + 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 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"); + HUDService.toggleBrowserConsole().then(() => { + info("reopen the browser console"); + executeSoon(() => + HUDService.toggleBrowserConsole().then(onBrowserConsoleReopen)); + }); + }); + privateWindow.BrowserTryToCloseWindow(); + } + + function onBrowserConsoleReopen(injectedHud) { + hud = injectedHud; + 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/devtools/client/webconsole/test/browser_console_server_logging.js b/devtools/client/webconsole/test/browser_console_server_logging.js new file mode 100644 index 0000000000..eaef12330f --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_server_logging.js @@ -0,0 +1,74 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +// Check that server log appears in the console panel - bug 1168872 +add_task(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging.sjs"; + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + // Set logging filter and wait till it's set on the backend + hud.setFilterState("serverlog", true); + yield updateServerLoggingListener(hud); + + BrowserReloadSkipCache(); + + // Note that the test is also checking out the (printf like) + // formatters and encoding of UTF8 characters (see the one at the end). + let text = "values: string Object { a: 10 } 123 1.12 \u2713"; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: text, + category: CATEGORY_SERVER, + severity: SEVERITY_LOG, + }], + }); + // Clean up filter + hud.setFilterState("serverlog", false); + yield updateServerLoggingListener(hud); +}); + +add_task(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging-array.sjs"; + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + // Set logging filter and wait till it's set on the backend + hud.setFilterState("serverlog", true); + yield updateServerLoggingListener(hud); + + BrowserReloadSkipCache(); + // Note that the test is also checking out the (printf like) + // formatters and encoding of UTF8 characters (see the one at the end). + let text = "Object { best: \"Firefox\", reckless: \"Chrome\", " + + "new_ie: \"Safari\", new_new_ie: \"Edge\" }"; + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: text, + category: CATEGORY_SERVER, + severity: SEVERITY_LOG, + }], + }); + // Clean up filter + hud.setFilterState("serverlog", false); + yield updateServerLoggingListener(hud); +}); + +function updateServerLoggingListener(hud) { + let deferred = promise.defer(); + hud.ui._updateServerLoggingListener(response => { + deferred.resolve(response); + }); + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_console_variables_view.js b/devtools/client/webconsole/test/browser_console_variables_view.js new file mode 100644 index 0000000000..ecd8071ce5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view.js @@ -0,0 +1,204 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that variables view works as expected in the web console. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-eval-in-stackframe.html"; + +var hud, gVariablesView; + +registerCleanupFunction(function () { + hud = gVariablesView = null; +}); + +add_task(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + + let msg = yield hud.jsterm.execute("(function foo(){})"); + + ok(msg, "output message found"); + ok(msg.textContent.includes("function foo()"), + "message text check"); + + executeSoon(() => { + EventUtils.synthesizeMouse(msg.querySelector("a"), 2, 2, {}, hud.iframeWindow); + }); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + ok(varView, "object inspector opened on click"); + + yield findVariableViewProperties(varView, [{ + name: "name", + value: "foo", + }], { webconsole: hud }); +}); + +add_task(function* () { + let msg = yield hud.jsterm.execute("fooObj"); + + ok(msg, "output message found"); + ok(msg.textContent.includes('{ testProp: "testValue" }'), + "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + let fetched = hud.jsterm.once("variablesview-fetched"); + + // executeSoon + EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.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); +}); + +function onFooObjFetch(aVar) { + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + return findVariableViewProperties(aVar, [ + { name: "testProp", value: "testValue" }, + ], { webconsole: hud }); +} + +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: hud + }); +} + +function onFooObjFetchAfterUpdate(aVar) { + info("onFooObjFetchAfterUpdate"); + let expectedValue = content.document.title + content.location + + "[object HTMLParagraphElement]"; + + return findVariableViewProperties(aVar, [ + { name: "testProp", value: expectedValue }, + ], { webconsole: hud }); +} + +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: hud + }); +} + +function* onFooObjFetchAfterPropRename(aVar) { + info("onFooObjFetchAfterPropRename"); + + let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + content.location + para; + }); + + // Check that the new value is in the variables view. + return findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: hud }); +} + +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: hud + }); +} + +function* onPropUpdateError(aVar) { + info("onPropUpdateError"); + + let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + content.location + para; + }); + + // Make sure the property did not change. + return findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: hud }); +} + +function onRenamedTestPropFoundAgain(aResults) { + let prop = aResults[0].matchedProp; + ok(prop, "matched the renamed |testProp| property again"); + + return waitForMessages({ + webconsole: hud, + 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/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js new file mode 100644 index 0000000000..522fe47546 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 ensures DOM nodes are rendered correctly in VariablesView. + +"use strict"; + +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", (_, val) => deferred.resolve(val)); + 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/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js new file mode 100644 index 0000000000..ec9ffa7b71 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js @@ -0,0 +1,135 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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')"); + // Object + yield testSorted("Object({'hello':1,1:5,10:2,4:2,'abc':1})"); + + // 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"); + } + /** + * A helper that ensures the properties are 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* testSorted(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 = ["1", "4", "10",..., "abc", "hello"] <- correct + // If the properties are not sorted: + // keys = ["1", "10", "4",...] <- incorrect + let keyIterator = variableScope._store.keys(); + is(keyIterator.next().value, "1", "First key should be 1"); + is(keyIterator.next().value, "4", "Second key should be 4"); + + // If the properties are sorted, the next one will be 10. + is(keyIterator.next().value, "10", "Third key is 10"); + // If sorted next properties should be "abc" then "hello" + is(keyIterator.next().value, "abc", "Fourth key is abc"); + is(keyIterator.next().value, "hello", "Fifth key is hello"); + } + + Task.spawn(runner).then(finishTest); +} diff --git a/devtools/client/webconsole/test/browser_console_variables_view_filter.js b/devtools/client/webconsole/test/browser_console_variables_view_filter.js new file mode 100644 index 0000000000..1424108397 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_filter.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that variables view filter feature works fine in the console. + +function props(view, prefix = "") { + // First match only the visible one, not hidden by a search + let visible = [...view].filter(([id, prop]) => prop._isMatch); + // Then flatten the list into a list of strings + // being the jsonpath of each attribute being visible in the view + return visible.reduce((list, [id, prop]) => { + list.push(prefix + id); + return list.concat(props(prop, prefix + id + ".")); + }, []); +} + +function assertAttrs(view, expected, message) { + is(props(view).join(","), expected, message); +} + +add_task(function* () { + yield loadTab("data:text/html;charset=utf-8,webconsole-filter"); + + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + + let fetched = jsterm.once("variablesview-fetched"); + + yield jsterm.execute("inspect({ foo: { bar : \"baz\" } })"); + + let view = yield fetched; + let variablesView = view._variablesView; + let searchbox = variablesView._searchboxNode; + + assertAttrs(view, "foo,__proto__", + "To start with, we just see the top level foo attr"); + + fetched = jsterm.once("variablesview-fetched"); + searchbox.value = "bar"; + searchbox.doCommand(); + view = yield fetched; + + assertAttrs(view, "", + "If we don't manually expand nested attr, we don't see them"); + + fetched = jsterm.once("variablesview-fetched"); + searchbox.value = ""; + searchbox.doCommand(); + view = yield fetched; + + assertAttrs(view, "foo", + "If we reset the search, we get back to original state"); + + yield [...view][0][1].expand(); + + fetched = jsterm.once("variablesview-fetched"); + searchbox.value = "bar"; + searchbox.doCommand(); + view = yield fetched; + + assertAttrs(view, "foo,foo.bar", "Now if we expand, we see the nested attr"); + + fetched = jsterm.once("variablesview-fetched"); + searchbox.value = "baz"; + searchbox.doCommand(); + view = yield fetched; + + assertAttrs(view, "foo,foo.bar", "We can also search for attr values"); + + fetched = jsterm.once("variablesview-fetched"); + searchbox.value = ""; + searchbox.doCommand(); + view = yield fetched; + + assertAttrs(view, "foo", + "If we reset again, we get back to original state again"); +}); diff --git a/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js new file mode 100644 index 0000000000..c1b2194dea --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js @@ -0,0 +1,97 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that variables view is linked to the inspector for highlighting and +// selecting DOM nodes + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-952277-highlight-nodes-in-vview.html"; + +var 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", onNodeListViewFetched); + + executeSoon(() => + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow) + ); +} + +function onNodeListViewFetched(event, variable) { + gVariablesView = variable._variablesView; + ok(gVariablesView, "variables view object"); + + // Transform the vview into an array we can filter properties from + let props = [...variable].map(([id, prop]) => [id, prop]); + + // 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]; + + 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/devtools/client/webconsole/test/browser_console_variables_view_special_names.js b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js new file mode 100644 index 0000000000..fa0dd4b924 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that variables view handles special names like "<return>" +// properly for ordinary displays. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 1084430"; + +add_task(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("inspect({ '<return>': 47, '<exception>': 91 })"); + + let varView = yield hud.jsterm.once("variablesview-fetched"); + ok(varView, "variables view object"); + + let props = yield findVariableViewProperties(varView, [ + { name: "<return>", value: 47 }, + { name: "<exception>", value: 91 }, + ], { webconsole: hud }); + + for (let prop of props) { + ok(!prop.matchedProp._internalItem, prop.name + " is not marked internal"); + let target = prop.matchedProp._target; + ok(!target.hasAttribute("pseudo-item"), + prop.name + " is not a pseudo-item"); + } +}); diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js new file mode 100644 index 0000000000..e83a8d6266 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js @@ -0,0 +1,109 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-eval-in-stackframe.html"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let dbgPanel = yield openDebugger(); + yield waitForFrameAdded(); + yield openConsole(); + yield testVariablesView(hud); +}); + +function* waitForFrameAdded() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + let thread = toolbox.threadClient; + + info("Waiting for framesadded"); + yield new Promise(resolve => { + thread.addOneTimeListener("framesadded", resolve); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.firstCall(); + }); + }); +} + +function* testVariablesView(hud) { + let jsterm = hud.jsterm; + let msg = yield jsterm.execute("fooObj"); + ok(msg, "output message found"); + ok(msg.textContent.includes('{ testProp2: "testValue2" }'), + "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + info("Waiting for variable view to appear"); + let variable = yield new Promise(resolve => { + jsterm.once("variablesview-fetched", (e, variable) => { + resolve(variable); + }); + executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {}, + hud.iframeWindow)); + }); + + info("Waiting for findVariableViewProperties"); + let results = yield findVariableViewProperties(variable, [ + { name: "testProp2", value: "testValue2" }, + { name: "testProp", value: "testValue", dontMatch: true }, + ], { webconsole: hud }); + + let prop = results[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. + variable = yield updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + foo2 + $('p')", + webconsole: hud + }); + + info("onFooObjFetchAfterUpdate"); + let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + "foo2SecondCall" + para; + }); + + results = yield findVariableViewProperties(variable, [ + { name: "testProp2", value: expectedValue }, + ], { webconsole: hud }); + + prop = results[0].matchedProp; + ok(prop, "matched the updated |testProp2| property value"); + + // Check that testProp2 was updated. + yield new Promise(resolve => { + executeSoon(() => { + jsterm.execute("fooObj.testProp2").then(resolve); + }); + }); + + expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + "foo2SecondCall" + para; + }); + + isnot(hud.outputNode.textContent.indexOf(expectedValue), -1, + "fooObj.testProp2 is correct"); +} diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js new file mode 100644 index 0000000000..556e7275d7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js @@ -0,0 +1,112 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 makes sure web console eval works while the js debugger paused the +// page, and while the inspector is active. See bug 886137. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-eval-in-stackframe.html"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let dbgPanel = yield openDebugger(); + yield openInspector(); + yield waitForFrameAdded(); + + yield openConsole(); + yield testVariablesView(hud); +}); + +function* waitForFrameAdded() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + let thread = toolbox.threadClient; + + info("Waiting for framesadded"); + yield new Promise(resolve => { + thread.addOneTimeListener("framesadded", resolve); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.firstCall(); + }); + }); +} + +function* testVariablesView(hud) { + info("testVariablesView"); + let jsterm = hud.jsterm; + + let msg = yield jsterm.execute("fooObj"); + ok(msg, "output message found"); + ok(msg.textContent.includes('{ testProp2: "testValue2" }'), + "message text check"); + + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + info("Waiting for variable view to appear"); + let variable = yield new Promise(resolve => { + jsterm.once("variablesview-fetched", (e, variable) => { + resolve(variable); + }); + executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {}, + hud.iframeWindow)); + }); + + info("Waiting for findVariableViewProperties"); + let results = yield findVariableViewProperties(variable, [ + { name: "testProp2", value: "testValue2" }, + { name: "testProp", value: "testValue", dontMatch: true }, + ], { webconsole: hud }); + + let prop = results[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. + variable = yield updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + foo2 + $('p')", + webconsole: hud + }); + + info("onFooObjFetchAfterUpdate"); + let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + "foo2SecondCall" + para; + }); + + results = yield findVariableViewProperties(variable, [ + { name: "testProp2", value: expectedValue }, + ], { webconsole: hud }); + + prop = results[0].matchedProp; + ok(prop, "matched the updated |testProp2| property value"); + + // Check that testProp2 was updated. + yield new Promise(resolve => { + executeSoon(() => { + jsterm.execute("fooObj.testProp2").then(resolve); + }); + }); + + expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let para = content.wrappedJSObject.document.querySelector("p"); + return content.document.title + "foo2SecondCall" + para; + }); + + isnot(hud.outputNode.textContent.indexOf(expectedValue), -1, + "fooObj.testProp2 is correct"); +} diff --git a/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js new file mode 100644 index 0000000000..bc923ff449 --- /dev/null +++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js @@ -0,0 +1,157 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 makes sure web console eval happens in the user-selected stackframe +// from the js debugger. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-eval-in-stackframe.html"; + +var gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController; +var gStackframes; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +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()"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + 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()|"); + + function runOpenConsole() { + openConsole().then(() => { + gJSTerm.execute("foo + foo2 + foo3").then(onExecuteFoo23InFirstCall); + }); + } + + executeSoon(() => { + gJSTerm.clearOutput(); + + info("openDebugger and selectFrame(1)"); + + openDebugger().then(() => { + gStackframes.selectFrame(1); + + info("openConsole"); + executeSoon(() => runOpenConsole()); + }); + }); +} + +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)); +} + +var onExecuteFooAndFoo3ChangesInFirstCall = Task.async(function*() { + let expected = "abbabug783499"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo3| updated in |firstCall()|"); + + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() { + 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/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js new file mode 100644 index 0000000000..bc116d443c --- /dev/null +++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js @@ -0,0 +1,71 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/" + + "test/test-eval-in-stackframe.html"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +add_task(function* () { + yield loadTab(TEST_URI); + + info("open the web console"); + let hud = yield openConsole(); + let {jsterm} = hud; + + info("open the debugger"); + let {panelWin} = yield openDebugger(); + let {DebuggerController} = panelWin; + let {activeThread} = 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/devtools/client/webconsole/test/browser_jsterm_inspect.js b/devtools/client/webconsole/test/browser_jsterm_inspect.js new file mode 100644 index 0000000000..aa18cbff65 --- /dev/null +++ b/devtools/client/webconsole/test/browser_jsterm_inspect.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that the inspect() jsterm helper function works. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>hello bug 869981"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + let jsterm = hud.jsterm; + + /* Check that the window object is inspected */ + jsterm.execute("testProp = 'testValue'"); + + let updated = jsterm.once("variablesview-updated"); + jsterm.execute("inspect(window)"); + let view = yield updated; + ok(view, "variables view object"); + + // The single variable view contains a scope with the variable name + // and unnamed subitem that contains the properties + let variable = view.getScopeAtIndex(0).get(undefined); + ok(variable, "variable object"); + + yield findVariableViewProperties(variable, [ + { name: "testProp", value: "testValue" }, + { name: "document", value: /HTMLDocument \u2192 data:/ }, + ], { webconsole: hud }); + + /* Check that a primitive value can be inspected, too */ + let updated2 = jsterm.once("variablesview-updated"); + jsterm.execute("inspect(1)"); + let view2 = yield updated2; + ok(view2, "variables view object"); + + // Check the label of the scope - it should contain the value + let scope = view.getScopeAtIndex(0); + ok(scope, "variable object"); + + is(scope.name, "1", "The value of the primitive var is correct"); +}); diff --git a/devtools/client/webconsole/test/browser_longstring_hang.js b/devtools/client/webconsole/test/browser_longstring_hang.js new file mode 100644 index 0000000000..036ad6e881 --- /dev/null +++ b/devtools/client/webconsole/test/browser_longstring_hang.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 very long strings do not hang the browser. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-859170-longstring-hang.html"; + +add_task(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/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js new file mode 100644 index 0000000000..f6f0575492 --- /dev/null +++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js @@ -0,0 +1,74 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " + + "displays requests that have been recorded in the " + + "web console, even if the netmonitor hadn't opened yet."; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" + + "test/" + TEST_FILE; + +const NET_PREF = "devtools.webconsole.filter.networkinfo"; +Services.prefs.setBoolPref(NET_PREF, true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref(NET_PREF); +}); + +add_task(function* () { + let { tab, browser } = yield loadTab(TEST_URI); + + // Test that the request appears in the console. + let hud = yield openConsole(); + info("Web console is open"); + + yield loadDocument(browser); + info("Document loaded."); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "network message", + text: TEST_FILE, + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG + } + ] + }); + + // Test that the request appears in the network panel. + let target = TargetFactory.forTab(tab); + let toolbox = yield gDevTools.showToolbox(target, "netmonitor"); + info("Network panel is open."); + + testNetmonitor(toolbox); +}); + +function loadDocument(browser) { + let deferred = promise.defer(); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve(); + }, true); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH); + + return deferred.promise; +} + +function testNetmonitor(toolbox) { + let monitor = toolbox.getCurrentPanel(); + let { RequestsMenu } = monitor.panelWin.NetMonitorView; + RequestsMenu.lazyUpdate = false; + + is(RequestsMenu.itemCount, 1, "Network request appears in the network panel"); + + let item = RequestsMenu.getItemAtIndex(0); + is(item.attachment.method, "GET", "The attached method is correct."); + is(item.attachment.url, TEST_PATH, "The attached url is correct."); +} diff --git a/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js new file mode 100644 index 0000000000..38a5b54198 --- /dev/null +++ b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Make sure that the Web Console output does not break after we try to call +// console.dir() for objects that are not inspectable. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,test for bug 773466"; + +add_task(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/devtools/client/webconsole/test/browser_output_longstring_expand.js b/devtools/client/webconsole/test/browser_output_longstring_expand.js new file mode 100644 index 0000000000..bae8ca1282 --- /dev/null +++ b/devtools/client/webconsole/test/browser_output_longstring_expand.js @@ -0,0 +1,85 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 long strings can be expanded in the console output. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,test for bug 787981 - check " + + "that long strings can be expanded in the output."; + +add_task(function* () { + let { DebuggerServer } = require("devtools/server/main"); + + 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/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js new file mode 100644 index 0000000000..36b13ce02f --- /dev/null +++ b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js @@ -0,0 +1,178 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/" + + "test/test-repeated-messages.html"; +const PREF = "devtools.webconsole.persistlog"; + +add_task(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); + yield testConsoleFalsyValues(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"); + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + 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, + }, + ], + }); +} + +function testConsoleFalsyValues(hud) { + hud.jsterm.clearOutput(true); + hud.jsterm.execute("testConsoleFalsyValues()"); + + info("wait for repeats of falsy values with the console API"); + + return waitForMessages({ + webconsole: hud, + messages: [ + { + name: "console.log 'NaN' repeated once", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 1, + }, + { + name: "console.log 'undefined' repeated once", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 1, + }, + { + name: "console.log 'null' repeated once", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 1, + }, + { + name: "console.log 'NaN' repeated twice", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2, + }, + { + name: "console.log 'undefined' repeated twice", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2, + }, + { + name: "console.log 'null' repeated twice", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2, + }, + ], + }); +} diff --git a/devtools/client/webconsole/test/browser_result_format_as_string.js b/devtools/client/webconsole/test/browser_result_format_as_string.js new file mode 100644 index 0000000000..0352d0afaf --- /dev/null +++ b/devtools/client/webconsole/test/browser_result_format_as_string.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Make sure that JS eval result are properly formatted as strings. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-result-format-as-string.html"; + +add_task(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; +} diff --git a/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js new file mode 100644 index 0000000000..0eeb6eaa32 --- /dev/null +++ b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js @@ -0,0 +1,86 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_REPLACED_API_URI = "http://example.com/browser/devtools/client/" + + "webconsole/test/test-console-replaced-api.html"; +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/testscript.js"; +const PREF = "devtools.webconsole.persistlog"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + let { browser } = yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield testWarningNotPresent(hud); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, 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(function () { + let browser = gBrowser.selectedBrowser; + ContentTask.spawn(browser, null, "() => 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); + let browser = gBrowser.selectedBrowser; + ContentTask.spawn(browser, null, "() => content.location.reload()"); + }); + }); + + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js new file mode 100644 index 0000000000..07f6372d01 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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 + +"use strict"; + +const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" + + "test/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" + + "Mixed_content" + DOCS_GA_PARAMS; + +add_task(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 " + + "\u201chttp://example.com/\u201d on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + { + name: "Logged mixed passive content - image", + text: "Loading mixed (insecure) display content " + + "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d " + + "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/devtools/client/webconsole/test/browser_webconsole_assert.js b/devtools/client/webconsole/test/browser_webconsole_assert.js new file mode 100644 index 0000000000..7fc9693f13 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_assert.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 console.assert() works as expected (i.e. outputs only on falsy +// asserts). See bug 760193. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-assert.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js new file mode 100644 index 0000000000..0ba1680786 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 that properties starting with underscores or dollars can be +// autocompleted (bug 967468). + +add_task(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 " + popup.itemCount + " suggestions for '" + term + "'"); + } + + let { jsterm } = yield openConsole(); + let popup = jsterm.autocompletePopup; + + yield jsterm.execute("var 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"); + + // Should work with bug 1207868. + yield jsterm.execute("let foobar = {a: ''}; const blargh = {a: 1};"); + yield autocomplete("foobar"); + yield autocomplete("blargh"); + yield autocomplete("foobar.a"); + yield autocomplete("blargh.a"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js new file mode 100644 index 0000000000..bcd2e22d0f --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the autocomplete input is being blurred and focused when selecting a value. +// This will help screen-readers notify users of the value that was set in the input. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test code completion"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + let input = jsterm.inputNode; + + info("Type 'd' to open the autocomplete popup"); + yield autocomplete(jsterm, "d"); + + // Add listeners for focus and blur events. + let wasBlurred = false; + input.addEventListener("blur", () => { + wasBlurred = true; + }, { + once: true + }); + + let wasFocused = false; + input.addEventListener("blur", () => { + ok(wasBlurred, "jsterm input received a blur event before received back the focus"); + wasFocused = true; + }, { + once: true + }); + + info("Close the autocomplete popup by simulating a TAB key event"); + let onPopupClosed = jsterm.autocompletePopup.once("popup-closed"); + EventUtils.synthesizeKey("VK_TAB", {}); + + info("Wait for the autocomplete popup to be closed"); + yield onPopupClosed; + + ok(wasFocused, "jsterm input received a focus event"); +}); + +function* autocomplete(jsterm, value) { + let popup = jsterm.autocompletePopup; + + yield new Promise(resolve => { + jsterm.setInputValue(value); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + ok(popup.isOpen && popup.itemCount > 0, + "Autocomplete popup is open and contains suggestions"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js new file mode 100644 index 0000000000..d0c6eb6732 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js @@ -0,0 +1,130 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642615"; + +XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; + +add_task(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); + }, + 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.getInputValue(); + 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.getInputValue(), "Paste blocked by self-xss prevention"); + + // Allow pasting + jsterm.setInputValue("allow pasting"); + let evt = document.createEvent("KeyboardEvent"); + evt.initKeyEvent("keyup", true, true, window, + 0, 0, 0, 0, + 0, " ".charCodeAt(0)); + jsterm.inputNode.dispatchEvent(evt); + jsterm.setInputValue(""); + goDoCommand("cmd_paste"); + isnot("", jsterm.getInputValue(), "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/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js new file mode 100644 index 0000000000..b4471bd6bc --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 autocomplete doesn't break when trying to reach into objects from +// a different domain, bug 989025. + +"use strict"; + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/devtools/client/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/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js new file mode 100644 index 0000000000..60ba5ff0eb --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js @@ -0,0 +1,245 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 makes sure web console autocomplete happens in the user-selected +// stackframe from the js debugger. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-autocomplete-in-stackframe.html"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +var gStackframes; +registerCleanupFunction(function () { + gStackframes = null; +}); + +requestLongerTimeout(2); +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + yield testCompletion(hud); +}); + +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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + newItems = popup.getItems(); + ok(!newItems.every(function (item) { + return item.label != "prop21"; + }), "autocomplete results do contain prop21"); + + info("Opening Debugger"); + let dbg = yield openDebugger(); + + info("Waiting for pause"); + yield pauseDebugger(dbg); + + info("Opening Console again"); + yield openConsole(); + + // From this point on the + // Test if 'f' gives 'foo3' and 'foo1' but not 'foo2' + input.value = "f"; + input.setSelectionRange(1, 1); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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"); + + yield openDebugger(); + + gStackframes.selectFrame(1); + + info("openConsole"); + yield openConsole(); + + // Test if 'f' gives 'foo2' and 'foo1' but not 'foo3' + input.value = "f"; + input.setSelectionRange(1, 1); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + 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); + yield new Promise(resolve => { + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve); + }); + + newItems = popup.getItems(); + is(newItems.length, 0, "no items for foo2Obj[0]"); +} + +function pauseDebugger(aResult) { + let debuggerWin = aResult.panelWin; + let debuggerController = debuggerWin.DebuggerController; + let thread = debuggerController.activeThread; + gStackframes = debuggerController.StackFrames; + return new Promise(resolve => { + thread.addOneTimeListener("framesadded", resolve); + + info("firstCall()"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.firstCall(); + }); + }); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js new file mode 100644 index 0000000000..afa3dd55d1 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js @@ -0,0 +1,27 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the autocomplete popup closes on switching tabs. See bug 900448. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 900448 - autocomplete " + + "popup closes on tab switch"; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + let popup = hud.jsterm.autocompletePopup; + let popupShown = once(popup, "popup-opened"); + + hud.jsterm.setInputValue("sc"); + EventUtils.synthesizeKey("r", {}); + + yield popupShown; + + yield loadTab("data:text/html;charset=utf-8,<p>testing autocomplete closes"); + + ok(!popup.isOpen, "Popup closes on tab switch"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js new file mode 100644 index 0000000000..ff4157a3b1 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js @@ -0,0 +1,110 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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 + +"use strict"; + +const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" + + "test/test-mixedcontent-securityerrors.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" + + "Mixed_content" + DOCS_GA_PARAMS; + +add_task(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 \u201chttp://example.com/\u201d", + category: CATEGORY_SECURITY, + severity: SEVERITY_ERROR, + objects: true, + }, + { + name: "Logged blocking mixed passive content - image", + text: "Blocked loading mixed active content \u201chttp://example.com/\u201d", + 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], + ["security.mixed_content.use_hsts", false], + ["security.mixed_content.send_hsts_priming", false], + ] + }; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} + +function mixedContentOverrideTest2(hud, browser) { + let deferred = promise.defer(); + let {gIdentityHandler} = browser.ownerGlobal; + ok(gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"), + "Mixed Active Content state appeared on identity box"); + gIdentityHandler.disableMixedContentProtection(); + + waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Logged blocking mixed active content", + text: "Loading mixed (insecure) active content " + + "\u201chttp://example.com/\u201d 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" + + " \u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d" + + " on a secure page", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + ], + }).then(msgs => deferred.resolve(msgs), e => console.error(e)); + + return deferred.promise; +} + +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/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js new file mode 100644 index 0000000000..ee141a72f5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab("data:text/html;charset=utf8,<title>Test for " + + "Bug 1006027"); + + 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) < 2000, + "console.log message timestamp spread < 2000ms confirmed"); + } +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js new file mode 100644 index 0000000000..ace13f8d18 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js @@ -0,0 +1,55 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +/* 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/devtools/client/" + + "webconsole/test/test_bug_1010953_cspro.html"; +const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " + + "blocked the loading of a resource at " + + "http://some.example.com/test.png " + + "(\u201cimg-src http://example.com\u201d)."; +const CSP_REPORT_MSG = "Content Security Policy: The page\u2019s settings " + + "observed the loading of a resource at " + + "http://some.example.com/test_bug_1010953_cspro.js " + + "(\u201cscript-src http://example.com\u201d). A CSP report is " + + "being sent."; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, TEST_VIOLATION); + yield loaded; + + 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/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js new file mode 100644 index 0000000000..9b220b4a24 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 clicking on a function displays its source in the debugger. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug_1050691_click_function_to_source.html"; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + // Open the Debugger panel. + let debuggerPanel = yield openDebugger(); + // And right after come back to the Console panel. + yield openConsole(); + yield testWithDebuggerOpen(hud, debuggerPanel); +}); + +function* testWithDebuggerOpen(hud, debuggerPanel) { + let clickable = yield printFunction(hud); + let panelWin = debuggerPanel.panelWin; + let onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET); + synthesizeClick(clickable, hud); + yield onEditorLocationSet; + ok(isDebuggerCaretPos(debuggerPanel, 7), + "Clicking on a function should go to its source in the debugger view"); +} + +function synthesizeClick(clickable, hud) { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); +} + +var printFunction = Task.async(function* (hud) { + hud.jsterm.clearOutput(); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.foo(); + }); + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + let msg = [...result.matched][0]; + let clickable = msg.querySelector("a"); + ok(clickable, "clickable item for object should exist"); + return clickable; +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js new file mode 100644 index 0000000000..26bac7f570 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console CSP messages for two META policies +// are correctly displayed. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test"; +const TEST_VIOLATION = "https://example.com/browser/devtools/client/" + + "webconsole/test/test_bug_1247459_violation.html"; +const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " + + "blocked the loading of a resource at " + + "http://some.example.com/test.png (\u201cimg-src " + + "https://example.com\u201d)."; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, TEST_VIOLATION); + yield loaded; + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "CSP policy URI warning displayed successfully", + text: CSP_VIOLATION_MSG, + repeats: 2 + } + ] + }); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js new file mode 100644 index 0000000000..fb0182d8b5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js @@ -0,0 +1,41 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the console object still exists after a page reload. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +var 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/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js new file mode 100644 index 0000000000..551dbd361b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the input field is focused when the console is opened. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js new file mode 100644 index 0000000000..4c5fbf9c80 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 to ensure that errors don't appear when the console is closed while a +// completion is being performed. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testClosingAfterCompletion(hud, browser); +}); + +function testClosingAfterCompletion(hud, browser) { + let deferred = promise.defer(); + + let errorWhileClosing = false; + function errorListener() { + errorWhileClosing = true; + } + + browser.addEventListener("error", errorListener, false); + + // Focus the jsterm and perform the keycombo to close the WebConsole. + hud.jsterm.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/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js new file mode 100644 index 0000000000..af00bf913a --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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 + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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"); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + 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/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js new file mode 100644 index 0000000000..6cd03164b0 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 appropriately-localized timestamps are printed. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + const TEST_TIMESTAMP = 12345678; + let date = new Date(TEST_TIMESTAMP); + let localizedString = WCUL10n.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/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js new file mode 100644 index 0000000000..5e7b141eb9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js @@ -0,0 +1,49 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/" + + "test/test-duplicate-error.html"; + +add_task(function* () { + yield loadTab(INIT_URI); + + let hud = yield openConsole(); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js new file mode 100644 index 0000000000..7dd2713880 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + testCompletion(hud); +}); + +function testCompletion(hud) { + let jsterm = hud.jsterm; + let 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/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js new file mode 100644 index 0000000000..974557ec05 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js @@ -0,0 +1,89 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console limits the number of lines displayed according to +// the user's preferences. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,test for bug 585237"; + +var outputNode; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + 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++) { + yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) { + // must change message to prevent repeats + content.console.log("foo #" + i); + }); + } + + 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"); + + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () { + content.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++) { + yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) { + // must change message to prevent repeats + content.console.log("boo #" + i); + }); + } + + 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/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js new file mode 100644 index 0000000000..c38fd52c14 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js @@ -0,0 +1,70 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/" + + "webconsole/test/test-bug-585956-console-trace.html"; + +add_task(function* () { + let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + let hud = yield openConsole(tab); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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: 3, + filename: TEST_URI, + functionName: "window.foobar585956c", + language: 2, + lineNumber: 9 + }, + { + columnNumber: 10, + filename: TEST_URI, + functionName: "foobar585956b", + language: 2, + lineNumber: 14 + }, + { + columnNumber: 10, + filename: TEST_URI, + functionName: "foobar585956a", + language: 2, + lineNumber: 18 + }, + { + columnNumber: 1, + 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/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js new file mode 100644 index 0000000000..0021a8cc1b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js @@ -0,0 +1,367 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " + + "popup keyboard usage test"; + +// We should turn off auto-multiline editing during these tests +const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; +var HUD, popup, jsterm, inputNode, completeNode; + +add_task(function* () { + Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, false); + 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; + Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, true); +}); + +var consoleOpened = Task.async(function* (hud) { + let deferred = promise.defer(); + HUD = hud; + 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.once("popup-opened", () => { + ok(popup.isOpen, "popup is open"); + + // 4 values, and the following properties: + // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ + // __proto__ hasOwnProperty isPrototypeOf propertyIsEnumerable + // toLocaleString toString toSource unwatch valueOf watch constructor. + is(popup.itemCount, 19, "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__", + "__proto__", + "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, 18, + "Index of the first item from bottom is selected."); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.getInputValue().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, 18, "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.once("popup-closed", () => { + deferred.resolve(); + }); + EventUtils.synthesizeKey("VK_TAB", {}); + }); + + 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(jsterm.getInputValue(), "window.foobarBug585991.watch", + "completion was successful after VK_TAB"); + + ok(!completeNode.value, "completeNode is empty"); + + popup.once("popup-opened", function onShown() { + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 19, "popup.itemCount is correct"); + + is(popup.selectedIndex, 18, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.getInputValue().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.once("popup-closed", function onHidden() { + ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); + + is(jsterm.getInputValue(), "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.once("popup-opened", function onShown() { + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 19, "popup.itemCount is correct"); + + is(popup.selectedIndex, 18, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); + + let prefix = jsterm.getInputValue().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.once("popup-closed", function onHidden() { + ok(!popup.isOpen, "popup is not open after VK_RETURN"); + + is(jsterm.getInputValue(), "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"); + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"]; + }); + + jsterm = HUD.jsterm; + popup = jsterm.autocompletePopup; + + popup.once("popup-opened", function onShown() { + let sameItems = popup.getItems().map(function (e) { + return e.label; + }); + ok(!sameItems.some(function (prop) { + prop === "0"; + }), "Completing on an array doesn't show numbers."); + + popup.once("popup-closed", function popupHidden() { + 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.once("popup-opened", function 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.once("popup-closed", function 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(jsterm.getInputValue(), "", "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.once("popup-opened", function 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.once("popup-closed", function 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(jsterm.getInputValue(), "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/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js new file mode 100644 index 0000000000..df1a42edf6 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js @@ -0,0 +1,123 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " + + "popup test"; + +add_task(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 = HUD.jsterm.inputNode; + + ok(!popup.isOpen, "popup is not open"); + ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant"); + + popup.once("popup-opened", () => { + 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 (item, index) { + return item === items[index]; + }), 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"); + checkActiveDescendant(popup, input); + + popup.selectedIndex = 1; + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + checkActiveDescendant(popup, input); + + popup.selectedItem = items[2]; + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + checkActiveDescendant(popup, input); + + is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works"); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + checkActiveDescendant(popup, input); + + is(popup.selectNextItem(), items[2], "selectNextItem() works"); + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + checkActiveDescendant(popup, input); + + ok(popup.selectNextItem(), "selectNextItem() works"); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem, items[0], "item0 is selected"); + checkActiveDescendant(popup, input); + + 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"); + checkActiveDescendant(popup, input); + + popup.removeItem(items[2]); + + is(popup.selectedIndex, 2, "index2 is selected"); + is(popup.selectedItem, items[3], "item3 is still selected"); + checkActiveDescendant(popup, input); + 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.once("popup-closed", () => { + deferred.resolve(); + }); + popup.hidePopup(); + }); + + popup.openPopup(input); + + return deferred.promise; +} + +function checkActiveDescendant(popup, input) { + let activeElement = input.ownerDocument.activeElement; + let descendantId = activeElement.getAttribute("aria-activedescendant"); + let popupItem = popup._tooltip.panel.querySelector("#" + descendantId); + let cloneItem = input.ownerDocument.querySelector("#" + descendantId); + + ok(popupItem, "Active descendant is found in the popup list"); + ok(cloneItem, "Active descendant is found in the list clone"); + is(popupItem.innerHTML, cloneItem.innerHTML, + "Cloned item has the same HTML as the original element"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js new file mode 100644 index 0000000000..cda31191ed --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testSelectionWhenMovingBetweenBoxes(hud); + performTestsAfterOutput(hud); +}); + +var testSelectionWhenMovingBetweenBoxes = Task.async(function* (hud) { + 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(hud) { + 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 = hud.ui.outputWrapper.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/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js new file mode 100644 index 0000000000..208baf3d65 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js @@ -0,0 +1,106 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ +/* globals goUpdateCommand goDoCommand */ + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +var HUD, outputNode; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield consoleOpened(hud); + yield testContextMenuCopy(); + + HUD = outputNode = null; +}); + +function consoleOpened(hud) { + HUD = hud; + + 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"); + + ContentTask.spawn(gBrowser.selectedBrowser, null, + "() => 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 and whitespace since getSelection() includes + // a new line 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) => { + // Strip out spaces for comparison ease + return selection.trim() == str.trim().replace(/ /g, ""); + }, () => { + 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 = HUD.ui.outputWrapper.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 and whitespace since getSelection() includes + // a new line 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) => { + // Strip out spaces for comparison ease + return selection.trim() == str.trim().replace(/ /g, ""); + }, () => { + goDoCommand("cmd_copy"); + }, deferred.resolve, deferred.resolve); + HUD = outputNode = null; + + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js new file mode 100644 index 0000000000..ff926fc138 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js @@ -0,0 +1,36 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 588342"; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield checkConsoleFocus(hud); + + let isFocused = yield ContentTask.spawn(browser, { }, function* () { + var fm = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + return fm.focusedWindow == content; + }); + + ok(isFocused, "content document has focus"); +}); + +function* checkConsoleFocus(hud) { + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + + yield new Promise(resolve => { + waitForFocus(resolve); + }); + + is(hud.jsterm.inputNode.getAttribute("focused"), "true", + "jsterm input is focused on web console open"); + is(fm.focusedWindow, hud.iframeWindow, "hud window is focused"); + yield closeConsole(null); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js new file mode 100644 index 0000000000..94a0ad77ee --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 adding text to one of the output labels doesn't cause errors. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js new file mode 100644 index 0000000000..c590495c4a --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js @@ -0,0 +1,44 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js new file mode 100644 index 0000000000..509c875f88 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +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 + */ +add_task(function* () { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + + // CSS warnings are disabled by default. + hud.setFilterState("cssparser", true); + hud.jsterm.clearOutput(); + + BrowserReload(); + + 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/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js new file mode 100644 index 0000000000..adbf130869 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js @@ -0,0 +1,29 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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, 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"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js new file mode 100644 index 0000000000..9f429a3d17 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js @@ -0,0 +1,68 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-593003-iframe-wrong-hud.html"; + +const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" + + "webconsole/test/test-bug-593003-iframe-wrong-" + + "hud-iframe.html"; + +const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" + + "webconsole/test/test-console.html"; + +add_task(function* () { + + let tab1 = (yield loadTab(TEST_URI)).tab; + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.console.log("FOO"); + }); + yield openConsole(); + + let tab2 = (yield loadTab(TEST_DUMMY_URI)).tab; + yield openConsole(gBrowser.selectedTab); + + info("Reloading tab 1"); + yield reloadTab(tab1); + + info("Checking for messages"); + yield checkMessages(tab1, tab2); + + info("Cleaning up"); + yield closeConsole(tab1); + yield closeConsole(tab2); +}); + +function* reloadTab(tab) { + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + yield loaded; +} + +function* checkMessages(tab1, tab2) { + let hud1 = yield openConsole(tab1); + let outputNode1 = hud1.outputNode; + + info("Waiting for messages"); + yield waitForMessages({ + webconsole: hud1, + messages: [{ + text: TEST_IFRAME_URI, + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }] + }); + + let hud2 = yield openConsole(tab2); + 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); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js new file mode 100644 index 0000000000..514f875c0c --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js @@ -0,0 +1,155 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +var jsterm, inputNode, values; + +var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 594497 and bug 619598"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + setup(hud); + performTests(); + + jsterm = inputNode = values = null; +}); + +function setup(HUD) { + jsterm = HUD.jsterm; + inputNode = jsterm.inputNode; + + jsterm.focus(); + + ok(!jsterm.getInputValue(), "jsterm.getInputValue() 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++) { + jsterm.setInputValue(values[i]); + jsterm.execute(); + } +} + +function performTests() { + EventUtils.synthesizeKey("VK_UP", {}); + + + is(jsterm.getInputValue(), values[4], + "VK_UP: jsterm.getInputValue() #4 is correct"); + + ok(inputNode.selectionStart == values[4].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.getInputValue(), values[3], + "VK_UP: jsterm.getInputValue() #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(jsterm.getInputValue(), values[3], + "VK_UP two times: jsterm.getInputValue() #3 is correct"); + + ok(inputNode.selectionStart == jsterm.getInputValue().indexOf("\n") && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.getInputValue(), values[3], + "VK_UP again: jsterm.getInputValue() #3 is correct"); + + ok(inputNode.selectionStart == 0 && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.getInputValue(), values[2], + "VK_UP: jsterm.getInputValue() #2 is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.getInputValue(), values[1], + "VK_UP: jsterm.getInputValue() #1 is correct"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(jsterm.getInputValue(), values[0], + "VK_UP: jsterm.getInputValue() #0 is correct"); + + ok(inputNode.selectionStart == values[0].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(jsterm.getInputValue(), values[1], + "VK_DOWN: jsterm.getInputValue() #1 is correct"); + + ok(inputNode.selectionStart == values[1].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(jsterm.getInputValue(), values[2], + "VK_DOWN: jsterm.getInputValue() #2 is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(jsterm.getInputValue(), values[3], + "VK_DOWN: jsterm.getInputValue() #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(jsterm.getInputValue(), values[3], + "VK_DOWN two times: jsterm.getInputValue() #3 is correct"); + + ok(inputNode.selectionStart > jsterm.getInputValue().lastIndexOf("\n") && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(jsterm.getInputValue(), values[3], + "VK_DOWN again: jsterm.getInputValue() #3 is correct"); + + ok(inputNode.selectionStart == values[3].length && + inputNode.selectionStart == inputNode.selectionEnd, + "caret location is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(jsterm.getInputValue(), values[4], + "VK_DOWN: jsterm.getInputValue() #4 is correct"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + ok(!jsterm.getInputValue(), + "VK_DOWN: jsterm.getInputValue() is empty"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js new file mode 100644 index 0000000000..d57d724caf --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const PREF = "devtools.webconsole.persistlog"; +const TEST_FILE = "test-network.html"; +const TEST_URI = "data:text/html;charset=utf8,<p>test file URI"; + +var hud; + +add_task(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); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js new file mode 100644 index 0000000000..1951cb3667 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js @@ -0,0 +1,100 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console doesn't leak when multiple tabs and windows are +// opened and then closed. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350"; + +var win1 = window, win2; +var openTabs = []; +var 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() { + function open(i) { + let tab = openTabs[i]; + openConsole(tab).then(function (hud) { + ok(hud, "HUD is open for tab " + i); + let window = hud.target.tab.linkedBrowser.contentWindow; + window.console.log("message for tab " + i); + + if (i >= openTabs.length - 1) { + // Use executeSoon() to allow the promise to resolve. + executeSoon(closeConsoles); + } + else { + executeSoon(() => open(i + 1)); + } + }); + } + + // open the Web Console for each of the four tabs and log a message. + open(0); +} + +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/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js new file mode 100644 index 0000000000..855cfbb885 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js @@ -0,0 +1,211 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +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/devtools/client/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", + }, + { + // #4 + file: "test-bug-595934-malformedxml.xhtml", + category: "malformed-xml", + matchString: "no root 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", + }, +]; + +var pos = -1; + +var foundCategory = false; +var foundText = false; +var pageLoaded = false; +var pageError = false; +var output = null; +var jsterm = null; +var hud = null; +var testEnded = false; + +var TestObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function testObserve(subject) { + if (testEnded || !(subject instanceof Ci.nsIScriptError)) { + return; + } + + let expectedCategory = TESTS[pos].category; + + info("test #" + pos + " console observer got " + subject.category + + ", is expecting " + expectedCategory); + + if (subject.category == expectedCategory) { + foundCategory = true; + startNextTest(); + } else { + info("unexpected message was: " + subject.sourceName + ":" + + subject.lineNumber + "; " + subject.errorMessage); + } + } +}; + +function consoleOpened(hudConsole) { + hud = hudConsole; + 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) { + 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(evt) { + if (content.location.href != testLocation) { + return; + } + gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true); + + pageLoaded = true; + test.onload && test.onload(evt); + + if (test.expectError) { + content.addEventListener("error", function _onError() { + content.removeEventListener("error", _onError); + pageError = true; + startNextTest(); + }); + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + } else { + pageError = true; + } + + startNextTest(); + }, true); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js new file mode 100644 index 0000000000..e14c3a069d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js @@ -0,0 +1,97 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +var tab1, tab2, win1, win2; +var 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(); + }); + } + + openConsoles(); +} + +function test() { + loadTab(TEST_URI).then(() => { + tab1 = gBrowser.selectedTab; + win1 = window; + tab1Loaded(); + }); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js new file mode 100644 index 0000000000..336700adae --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js @@ -0,0 +1,33 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/" + + "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); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + BrowserTestUtils.synthesizeMouseAtCenter("button", {}, gBrowser.selectedBrowser); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bogus is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); + }).then(finishTest); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js new file mode 100644 index 0000000000..473f02ccc8 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 network requests from chrome don't cause the Web Console to +// throw exceptions. + +"use strict"; + +const TEST_URI = "http://example.com/"; + +var good = true; +var listener = { + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]), + observe: function (subject) { + if (subject instanceof Ci.nsIScriptError && + subject.category === "XPConnect JavaScript" && + subject.sourceName.includes("webconsole")) { + good = false; + } + } +}; + +var xhr; + +function test() { + Services.console.registerListener(listener); + + // trigger a lazy-load of the HUD Service + HUDService; + + 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/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js new file mode 100644 index 0000000000..2de4c9f218 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-network.html"; +const PREF = "devtools.webconsole.persistlog"; + +add_task(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.ui.outputWrapper; + 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/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js new file mode 100644 index 0000000000..5a8280eed9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js @@ -0,0 +1,70 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-597756-reopen-closed-tab.html"; + +var HUD; + +add_task(function* () { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + let { browser } = yield loadTab(TEST_URI); + HUD = yield openConsole(); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + yield reload(browser); + + yield testMessages(); + + yield closeConsole(); + + // Close and reopen + gBrowser.removeCurrentTab(); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + let tab = yield loadTab(TEST_URI); + HUD = yield openConsole(); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + yield reload(tab.browser); + + yield testMessages(); + + HUD = null; +}); + +function reload(browser) { + let loaded = loadBrowser(browser); + browser.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/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js new file mode 100644 index 0000000000..4849793cb5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const INIT_URI = "data:text/plain;charset=utf8,hello world"; +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-599725-response-headers.sjs"; + +function performTest(request, hud) { + let deferred = promise.defer(); + + let headers = null; + + function readHeader(name) { + for (let header of headers) { + if (header.name == name) { + return header.value; + } + } + return null; + } + + hud.ui.proxy.webConsoleClient.getResponseHeaders(request.actor, + function (response) { + headers = response.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"); + + executeSoon(deferred.resolve); + }); + + return deferred.promise; +} + +let waitForRequest = Task.async(function*(hud) { + let request = yield waitForFinishedRequest(req=> { + return req.response.status === "304"; + }); + + yield performTest(request, hud); +}); + +add_task(function* () { + let { browser } = yield loadTab(INIT_URI); + + let hud = yield openConsole(); + + let gotLastRequest = waitForRequest(hud); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, TEST_URI); + yield loaded; + + let reloaded = loadBrowser(browser); + ContentTask.spawn(browser, null, "() => content.location.reload()"); + yield reloaded; + + yield gotLastRequest; +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js new file mode 100644 index 0000000000..153863824b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const INIT_URI = "data:text/html;charset=utf-8,Web Console - bug 600183 test"; +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-600183-charset.html"; + +function performTest(lastFinishedRequest, console) { + let deferred = promise.defer(); + + ok(lastFinishedRequest, "charset test page was loaded and logged"); + HUDService.lastFinishedRequest.callback = null; + + executeSoon(() => { + console.webConsoleClient.getResponseContent(lastFinishedRequest.actor, + (response) => { + ok(!response.contentDiscarded, "response body was not discarded"); + + let body = response.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; +} + +add_task(function* () { + let { browser } = yield loadTab(INIT_URI); + + yield openConsole(); + + let gotLastRequest = waitForRequest(); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, TEST_URI); + yield loaded; + + yield gotLastRequest; +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js new file mode 100644 index 0000000000..9dd81c9fd7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 601177: log levels"; +const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-601177-log-levels.html"; + +add_task(function* () { + Services.prefs.setBoolPref("javascript.options.strict", true); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + yield testLogLevels(hud); + + Services.prefs.clearUserPref("javascript.options.strict"); +}); + +function testLogLevels(hud) { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js new file mode 100644 index 0000000000..89bd83a7aa --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the console output scrolls to JS eval results when there are many +// messages displayed. See bug 601352. + +"use strict"; + +add_task(function* () { + let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test " + + "for bug 601352"); + let hud = yield openConsole(tab); + hud.jsterm.clearOutput(); + + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + 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.ui.outputWrapper; + let rectNode = node.getBoundingClientRect(); + let rectOutput = scrollNode.getBoundingClientRect(); + + yield ContentTask.spawn(gBrowser.selectedBrowser, { + rectNode, + rectOutput, + scrollHeight: scrollNode.scrollHeight, + scrollTop: scrollNode.scrollTop, + clientHeight: scrollNode.clientHeight, + }, function* (args) { + console.debug("rectNode", args.rectNode, "rectOutput", args.rectOutput); + console.log("scrollNode scrollHeight", args.scrollHeight, + "scrollTop", args.scrollTop, "clientHeight", + args.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/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js new file mode 100644 index 0000000000..6dae0a7b7e --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js @@ -0,0 +1,267 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the filter button UI logic works correctly. + +"use strict"; + +const TEST_URI = "http://example.com/"; +const FILTER_PREF_DOMAIN = "devtools.webconsole.filter."; + +var hud, hudId, hudBox; +var prefs = {}; + +add_task(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + hudId = hud.hudId; + hudBox = hud.ui.rootElement; + + savePrefs(); + + testFilterButtons(); + + restorePrefs(); + + hud = hudId = hudBox = null; +}); + +function savePrefs() { + let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN); + let children = branch.getChildList(""); + for (let child of children) { + prefs[child] = branch.getBoolPref(child); + } +} + +function restorePrefs() { + let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN); + for (let p in prefs) { + branch.setBoolPref(p, prefs[p]); + } +} + +function testFilterButtons() { + testMenuFilterButton("net"); + testMenuFilterButton("css"); + testMenuFilterButton("js"); + testMenuFilterButton("logging"); + testMenuFilterButton("security"); + testMenuFilterButton("server"); + + testIsolateFilterButton("net"); + testIsolateFilterButton("css"); + testIsolateFilterButton("js"); + testIsolateFilterButton("logging"); + testIsolateFilterButton("security"); + testIsolateFilterButton("server"); +} + +function testMenuFilterButton(category) { + let selector = ".webconsole-filter-button[category=\"" + category + "\"]"; + let button = hudBox.querySelector(selector); + ok(button, "we have the \"" + category + "\" button"); + + let firstMenuItem = button.querySelector("menuitem"); + ok(firstMenuItem, "we have the first menu item for the \"" + category + + "\" 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 " + + category + " 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 " + category + " 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 " + + category + " 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 " + category + " 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 " + category); + + clickButton(subbutton); + ok(!isChecked(button), "the button for category " + category + " is " + + "no longer checked after clicking its main part"); + + menuItem = firstMenuItem; + while (menuItem) { + prefKey = menuItem.getAttribute("prefKey"); + if (prefKey) { + ok(!isChecked(menuItem), "menu item " + prefKey + " for category " + + category + " 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 " + category + " is " + + "checked after clicking its main part"); + + menuItem = firstMenuItem; + while (menuItem) { + if (menuItem.hasAttribute("prefKey")) { + prefKey = menuItem.getAttribute("prefKey"); + // The CSS/Log menu item should not be checked. See bug 971798. + if (category == "css" && prefKey == "csslog") { + ok(!isChecked(menuItem), "menu item " + prefKey + " for category " + + category + " 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 " + + category + " 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 " + category + " is " + + "unchecked after unchecking all its filters"); + + // Turn all the filters on again by clicking the button. + clickButton(subbutton); +} + +function testIsolateFilterButton(category) { + let selector = ".webconsole-filter-button[category=\"" + category + "\"]"; + let targetButton = hudBox.querySelector(selector); + ok(targetButton, "we have the \"" + category + "\" button"); + + // Get the main part of the filter button. + let subbutton = getMainButton(targetButton); + ok(subbutton, "we have the subbutton for category " + category); + + // Turn on all the filters by alt clicking the main part of the button. + altClickButton(subbutton); + ok(isChecked(targetButton), "the button for category " + category + + " 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 (category == "css" && prefKey == "csslog") { + ok(!isChecked(item), "menu item " + prefKey + " for category " + + category + " should not be checked after isolating for " + category); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages should be " + + "turned off after isolating for " + category); + } else if (prefKey) { + ok(isChecked(item), "menu item " + prefKey + " for category " + + category + " is checked after isolating for " + category); + ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "turned on after isolating for " + category); + } + }); + + // 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 categoryBtn = filterButton.getAttribute("category"); + ok(!isChecked(filterButton), "the button for category " + + categoryBtn + " is unchecked after isolating for " + category); + + menuItems = filterButton.querySelectorAll("menuitem"); + Array.forEach(menuItems, (item) => { + let prefKey = item.getAttribute("prefKey"); + if (prefKey) { + ok(!isChecked(item), "menu item " + prefKey + " for category " + + category + " is unchecked after isolating for " + category); + ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " + + "turned off after isolating for " + category); + } + }); + + // 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(targetButton) { + let anonymousNodes = hud.ui.document.getAnonymousNodes(targetButton); + 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(node) { + EventUtils.sendMouseEvent({ type: "click" }, node); +} + +function altClickButton(node) { + EventUtils.sendMouseEvent({ type: "click", altKey: true }, node); +} + +function chooseMenuItem(node) { + let event = document.createEvent("XULCommandEvent"); + event.initCommandEvent("command", true, true, window, 0, false, false, false, + false, null); + node.dispatchEvent(event); +} + +function isChecked(node) { + return node.getAttribute("checked") === "true"; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js new file mode 100644 index 0000000000..f14530d06b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/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"; + +add_task(function* () { + yield loadTab(TEST_URI2); + + let hud = yield openConsole(); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_611795.js b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js new file mode 100644 index 0000000000..1fa4d717e2 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +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>"; +var hud; + +/** + * Unit test for bug 611795: + * Repeated CSS messages get collapsed into one. + */ + +add_task(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 \u2018-moz-opacity\u2019. 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/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js new file mode 100644 index 0000000000..5d00679580 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-613013-console-api-iframe.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js new file mode 100644 index 0000000000..95752021d3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613280"; + +function test() { + loadTab(TEST_URI).then(() => { + openConsole().then((HUD) => { + ContentTask.spawn(gBrowser.selectedBrowser, null, function*(){ + 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 clipboardSetup = function () { + goDoCommand("cmd_copy"); + }; + + let clipboardCopyDone = function () { + finishTest(); + }; + + 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 and whitespace since getSelection() includes + // a new line 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 selectionText.trim() === str.trim().replace(/ /g, ""); + }, clipboardSetup, clipboardCopyDone, clipboardCopyDone); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js new file mode 100644 index 0000000000..e24ce28e2d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 613642: remember scroll location"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + let outputNode = hud.outputNode; + let scrollBox = hud.ui.outputWrapper; + + for (let i = 0; i < 150; i++) { + ContentTask.spawn(gBrowser.selectedBrowser, i, function* (num) { + content.console.log("test message " + num); + }); + } + + 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 + ContentTask.spawn(gBrowser.selectedBrowser, null, + "() => 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; + + ContentTask.spawn(gBrowser.selectedBrowser, null, + "() => 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/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js new file mode 100644 index 0000000000..c53fe16833 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js @@ -0,0 +1,82 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 613642: maintain scroll with pruning of old messages"; + +var hud; + +add_task(function* () { + yield loadTab(TEST_URI); + + hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let outputNode = hud.outputNode; + + Services.prefs.setIntPref("devtools.hud.loglimit.console", 140); + let scrollBoxElement = hud.ui.outputWrapper; + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + 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 + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + 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/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js new file mode 100644 index 0000000000..ae61023a9e --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 614793: jsterm result scroll"; + +requestLongerTimeout(2); + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield testScrollPosition(hud); +}); + +function* testScrollPosition(hud) { + hud.jsterm.clearOutput(); + + let scrollNode = hud.ui.outputWrapper; + + for (let i = 0; i < 150; i++) { + yield ContentTask.spawn(gBrowser.selectedBrowser, i, function* (i) { + content.console.log("test message " + i); + }); + } + + let oldScrollTop = -1; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "test message 149", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + oldScrollTop = scrollNode.scrollTop; + isnot(oldScrollTop, 0, "scroll location is not at the top"); + + let msg = yield hud.jsterm.execute("'hello world'"); + + isnot(scrollNode.scrollTop, oldScrollTop, "scroll location updated"); + + oldScrollTop = scrollNode.scrollTop; + + msg.scrollIntoView(false); + + is(scrollNode.scrollTop, oldScrollTop, "scroll location is the same"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js new file mode 100644 index 0000000000..439793b223 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js @@ -0,0 +1,36 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 we report JS exceptions in event handlers coming from +// network requests, like onreadystate for XHR. See bug 618078. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 618078"; +const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-618078-network-exceptions.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "bug618078exception", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }], + }); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js new file mode 100644 index 0000000000..6f4248c51a --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-621644-jsterm-dollar.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js new file mode 100644 index 0000000000..f4b5dca96f --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js @@ -0,0 +1,149 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const prefs = { + "net": [ + "network", + "netwarn", + "netxhr", + "networkinfo" + ], + "css": [ + "csserror", + "cssparser", + "csslog" + ], + "js": [ + "exception", + "jswarn", + "jslog", + ], + "logging": [ + "error", + "warn", + "info", + "log", + "serviceworkers", + "sharedworkers", + "windowlessworkers" + ] +}; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js new file mode 100644 index 0000000000..3de10774d8 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 for https://bugzilla.mozilla.org/show_bug.cgi?id=623749 +// Map Control + A to Select All, In the web console input, on Windows + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Test console for bug 623749"; + +add_task(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, jsterm.getInputValue().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/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js new file mode 100644 index 0000000000..5097499538 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js @@ -0,0 +1,120 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " + + "bug 630733"; +const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-630733-response-redirect-headers.sjs"; + +var lastFinishedRequests = {}; +var webConsoleClient; + +add_task(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; + HUDService.lastFinishedRequest.callback = (aHttpRequest) => { + let status = aHttpRequest.response.status; + lastFinishedRequests[status] = aHttpRequest; + if ("301" in lastFinishedRequests && + "404" in lastFinishedRequests) { + deferred.resolve(); + } + }; + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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 (response) { + lastFinishedRequests["301"].response.headers = response.headers; + + webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor, + function (resp) { + lastFinishedRequests["404"].response.headers = resp.headers; + executeSoon(deferred.resolve); + }); + }); + return deferred.promise; +} + +function getContent() { + let deferred = promise.defer(); + + webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor, + function (response) { + lastFinishedRequests["301"].response.content = response.content; + lastFinishedRequests["301"].discardResponseBody = response.contentDiscarded; + + webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor, + function (resp) { + lastFinishedRequests["404"].response.content = resp.content; + lastFinishedRequests["404"].discardResponseBody = + resp.contentDiscarded; + + webConsoleClient = null; + executeSoon(deferred.resolve); + }); + }); + return deferred.promise; +} + +function performTest() { + function readHeader(name) { + for (let header of headers) { + if (header.name == name) { + 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/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js new file mode 100644 index 0000000000..45d1f7102d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-632275-getters.html"; + +var 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/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js new file mode 100644 index 0000000000..c5e672444d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-632347-iterators-generators.html"; + +function test() { + requestLongerTimeout(6); + + loadTab(TEST_URI).then(() => { + openConsole().then(consoleOpened); + }); +} + +function consoleOpened(HUD) { + let {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider"); + + 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.addDebuggee(content); + 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().value; + + completion = JSPropertyProvider(dbgWindow, null, "_container.gen2."); + isnot(completion.matches.length, 0, "Got matches for gen2"); + + is((result / 2 + 1) * 2, container.gen2.next().value, + "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)"); + + dbg.removeDebuggee(content); + 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/devtools/client/webconsole/test/browser_webconsole_bug_632817.js b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js new file mode 100644 index 0000000000..561e3b112d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js @@ -0,0 +1,217 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 Web console messages can be filtered for NET events. + +"use strict"; + +const TEST_NETWORK_REQUEST_URI = + "https://example.com/browser/devtools/client/webconsole/test/" + + "test-network-request.html"; + +const TEST_IMG = "http://example.com/browser/devtools/client/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"; + +const PAGE_REQUEST_PREDICATE = + ({ request }) => request.url.endsWith("test-network-request.html"); + +const TEST_DATA_REQUEST_PREDICATE = + ({ request }) => request.url.endsWith("test-data.json"); + +const XHR_WARN_REQUEST_PREDICATE = + ({ request }) => request.url.endsWith("sjs_cors-test-server.sjs"); + +let hud; + +add_task(function*() { + 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); + hud = null; + }); + + yield loadTab(TEST_URI); + hud = yield openConsole(); + + yield testPageLoad(); + yield testXhrGet(); + yield testXhrWarn(); + yield testXhrPost(); + yield testFormSubmission(); + yield testLiveFilteringOnSearchStrings(); +}); + +function testPageLoad() { + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_NETWORK_REQUEST_URI); + let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE); + + // 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"); +} + +function testXhrGet() { + // Start the XMLHttpRequest() GET test. + ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() { + content.wrappedJSObject.testXhrGet(); + }); + + let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE); + + ok(lastRequest, "testXhrGet() was logged"); + is(lastRequest.request.method, "GET", "Method is correct"); + ok(lastRequest.isXHR, "It's an XHR request"); +} + +function testXhrWarn() { + // Start the XMLHttpRequest() warn test. + ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() { + content.wrappedJSObject.testXhrWarn(); + }); + + let lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE); + if (lastRequest.request.method == "HEAD") { + // in non-e10s, we get the HEAD request that priming sends, so make sure + // a priming request should be sent, and then get the actual request + is(Services.prefs.getBoolPref("security.mixed_content.send_hsts_priming"), + true, "Found HSTS Priming Request"); + lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE); + } + + ok(lastRequest, "testXhrWarn() was logged"); + is(lastRequest.request.method, "GET", "Method is correct"); + ok(lastRequest.isXHR, "It's an XHR request"); + is(lastRequest.securityInfo, "insecure", "It's an insecure request"); +} + +function testXhrPost() { + // Start the XMLHttpRequest() POST test. + ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() { + content.wrappedJSObject.testXhrPost(); + }); + + let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE); + + ok(lastRequest, "testXhrPost() was logged"); + is(lastRequest.request.method, "POST", "Method is correct"); + ok(lastRequest.isXHR, "It's an XHR request"); +} + +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. + ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() { + let form = content.document.querySelector("form"); + ok(form, "we have the HTML form"); + form.submit(); + }); + + // The form POSTs to the page URL but over https (page over http). + let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE); + + 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, + }, + ], + }); +} + +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'\""); +} + +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(value) { + hud.ui.filterBox.value = value; + hud.ui.adjustVisibilityOnSearchStringChange(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js new file mode 100644 index 0000000000..caaa736280 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console limits the number of lines displayed according to +// the user's preferences. + +"use strict"; + +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, + }, + { + // LOG_LIMIT + 5 + text: "css log 24", + 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/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js new file mode 100644 index 0000000000..93063e4364 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js @@ -0,0 +1,235 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console limits the number of lines displayed according to +// the limit set for each category. + +"use strict"; + +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/devtools/client/" + + "webconsole/test/test-bug-644419-log-limits.html"; + +var hud, outputNode; + +add_task(function* () { + let { browser } = yield loadTab(INIT_URI); + + hud = yield openConsole(); + + hud.jsterm.clearOutput(); + outputNode = hud.outputNode; + + let loaded = loadBrowser(browser); + + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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++) { + let script = content.document.createElement("script"); + script.text = "fubar" + i + ".bogus(6);"; + + if (!Services.appinfo.browserTabsRemoteAutostart) { + 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 true; + } + + 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++) { + let 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 \u2018-moz-foobar0\u2019", + "first message is pruned", false, true); + findLogEntry("Unknown property \u2018-moz-foobar1\u2019"); + // Check if the sentinel entry is still there. + findLogEntry("testing CSS limits"); + + Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser"); + }); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js new file mode 100644 index 0000000000..81573e56fe --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 logging methods display the method location along with +// the output in the console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console file location " + + "display test"; +const TEST_URI2 = "http://example.com/browser/devtools/client/" + + "webconsole/test/" + + "test-bug-646025-console-file-location.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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: 8 }, + }, + { + text: "message for level info", + category: CATEGORY_WEBDEV, + severity: SEVERITY_INFO, + source: { url: "test-file-location.js", line: 9 }, + }, + { + text: "message for level warn", + category: CATEGORY_WEBDEV, + severity: SEVERITY_WARNING, + source: { url: "test-file-location.js", line: 10 }, + }, + { + text: "message for level error", + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + source: { url: "test-file-location.js", line: 11 }, + }, + { + text: "message for level debug", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + source: { url: "test-file-location.js", line: 12 }, + }], + }); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js new file mode 100644 index 0000000000..233643d51d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 document.body autocompletes in the web console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console autocompletion " + + "bug in document.body"; + +var gHUD; + +add_task(function* () { + yield loadTab(TEST_URI); + + gHUD = yield openConsole(); + + yield consoleOpened(); + yield autocompletePopupHidden(); + let view = yield testPropertyPanel(); + yield onVariablesViewReady(view); + + gHUD = null; +}); + +function consoleOpened() { + let deferred = promise.defer(); + + let jsterm = gHUD.jsterm; + let popup = jsterm.autocompletePopup; + + ok(!popup.isOpen, "popup is not open"); + + popup.once("popup-opened", () => { + 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.once("popup-closed", () => { + deferred.resolve(); + }); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + + 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; + + 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", (evt, view) => { + deferred.resolve(view); + }); + let anchor = msg.querySelector(".message-body a"); + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow); + }); + + return deferred.promise; +} + +function onVariablesViewReady(view) { + return findVariableViewProperties(view, [ + { name: "body", value: "<body>" }, + ], { webconsole: gHUD }); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js new file mode 100644 index 0000000000..217d481e25 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js @@ -0,0 +1,109 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the $0 console helper works as intended. + +"use strict"; + +var 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); +} + +var 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 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); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, + "data:text/html;charset=utf-8,test for highlighter helper in web console"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js new file mode 100644 index 0000000000..2c6c933fc7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Console API implements the time() and timeEnd() methods. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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>"; + +add_task(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. + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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. + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI4); + yield loadBrowser(browser); + + testLogEntry(hud2.outputNode, "bTimer: timer started", + "bTimer was not started", false, true); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js new file mode 100644 index 0000000000..03741a249b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js @@ -0,0 +1,36 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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.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"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js new file mode 100644 index 0000000000..5906d62d64 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>bug 660806 - history " + + "navigation must not show the autocomplete popup"; + +add_task(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.on("popup-opened", onShown); + + 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.off("popup-opened", onShown); + executeSoon(deferred.resolve); + }); + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js new file mode 100644 index 0000000000..fd510240e4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js @@ -0,0 +1,79 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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.group/groupEnd works as intended. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "bug 664131: Expand console object with group methods"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + let jsterm = hud.jsterm; + + 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/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js new file mode 100644 index 0000000000..2b1588ef9b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the autocompletion results contain the names of JSTerm helpers. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test JSTerm Helpers " + + "autocomplete"; + +var jsterm; + +add_task(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; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_704295.js b/devtools/client/webconsole/test/browser_webconsole_bug_704295.js new file mode 100644 index 0000000000..df21232cf6 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_704295.js @@ -0,0 +1,41 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 for bug 704295 + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + testCompletion(hud); +}); + +function testCompletion(hud) { + let jsterm = hud.jsterm; + let 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/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js new file mode 100644 index 0000000000..af9e172c82 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/browser/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + let 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/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js new file mode 100644 index 0000000000..4665af42a3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console Mixed Content messages are displayed + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test"; +const TEST_HTTPS_URI = "https://example.com/browser/devtools/client/" + + "webconsole/test/test-bug-737873-mixedcontent.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" + + "Mixed_content"; + +registerCleanupFunction(function*() { + Services.prefs.clearUserPref("security.mixed_content.block_display_content"); + Services.prefs.clearUserPref("security.mixed_content.block_active_content"); +}); + +add_task(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); +}); + +var testMixedContent = Task.async(function* (hud) { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js new file mode 100644 index 0000000000..85b99a79ae --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 warnings about ineffective iframe sandboxing are logged to the +// web console when necessary (and not otherwise). + +"use strict"; + +requestLongerTimeout(2); + +const TEST_URI_WARNING = "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html"; +const TEST_URI_NOWARNING = [ + "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html", + "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html", + "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html", + "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html", + "http://example.com/browser/devtools/client/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"; + +add_task(function* () { + yield testYesWarning(); + + for (let id = 0; id < TEST_URI_NOWARNING.length; id++) { + yield testNoWarning(id); + } +}); + +function* testYesWarning() { + yield loadTab(TEST_URI_WARNING); + let hud = yield openConsole(); + + ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) { + content.console.log(msg); + }); + + yield 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 + } + ] + }); + + let msgs = hud.outputNode.querySelectorAll(".message[category=security]"); + is(msgs.length, 1, "one security message"); +} + +function* testNoWarning(id) { + yield loadTab(TEST_URI_NOWARNING[id]); + let hud = yield openConsole(); + + ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) { + content.console.log(msg); + }); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: SENTINEL_MSG, + severity: SEVERITY_LOG + } + ] + }); + + let msgs = hud.outputNode.querySelectorAll(".message[category=security]"); + is(msgs.length, 0, "no security messages (case " + id + ")"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js new file mode 100644 index 0000000000..49df4d1fc6 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 errors about insecure passwords are logged to the web console. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html"; +const INSECURE_PASSWORD_MSG = "Password fields present on an insecure " + + "(http://) iframe. This is a security risk that allows user login " + + "credentials to be stolen."; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js new file mode 100644 index 0000000000..00a620fc89 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js @@ -0,0 +1,62 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 errors about insecure passwords are logged to the web console. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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/Web/" + + "Security/Insecure_passwords" + DOCS_GA_PARAMS; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js new file mode 100644 index 0000000000..731e79d8b2 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js @@ -0,0 +1,142 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// This is a test for the Open URL context menu item +// that is shown for network requests + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; +const COMMAND_NAME = "consoleCmd_openURL"; +const CONTEXT_MENU_ID = "#menu_openURL"; + +var HUD = null, outputNode = null, contextMenu = null; + +add_task(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 testOnNetActivityContextMenu(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(results) { + outputNode.focus(); + outputNode.selectedItem = [...results[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."); + + 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(results) { + let deferred = promise.defer(); + + outputNode.focus(); + let msg = [...results[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(evt) { + gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true); + newTab = evt.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 testOnNetActivityContextMenu(msg) { + let deferred = promise.defer(); + + outputNode.focus(); + HUD.ui.output.selectMessage(msg); + + 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/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js new file mode 100644 index 0000000000..3686fba895 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js @@ -0,0 +1,88 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/test" + + "/test-bug-766001-js-console-links.html"; + +// Force the new debugger UI, in case this gets uplifted with the old +// debugger still turned on +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +function test() { + let hud; + + requestLongerTimeout(2); + Task.spawn(runner).then(finishTest); + + function* runner() { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + 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 > .frame-link"), + consoleMsg.querySelector(".message-location > .frame-link")]; + 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("data-url"); + ok(url, "source url found for index " + index); + + let line = node.getAttribute("data-line"); + ok(line, "found source line for index " + index); + + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "click" }, node.querySelector(".frame-link-filename")); + }); + + yield hud.ui.once("source-in-debugger-opened"); + + let toolbox = yield gDevTools.getToolbox(hud.target); + let dbg = toolbox.getPanel("jsdebugger"); + is(dbg._selectors().getSelectedSource(dbg._getState()).get("url"), + url, + "expected source url"); + } +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js new file mode 100644 index 0000000000..3a7134202d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console CSP messages are displayed + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test"; +const TEST_VIOLATION = "https://example.com/browser/devtools/client/" + + "webconsole/test/test_bug_770099_violation.html"; +const CSP_VIOLATION_MSG = "Content Security Policy: The page\u2019s settings " + + "blocked the loading of a resource at " + + "http://some.example.com/test.png (\u201cdefault-src " + + "https://example.com\u201d)."; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js new file mode 100644 index 0000000000..f2efd79227 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js @@ -0,0 +1,140 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test" + + "/test-bug-782653-css-errors.html"; + +var nodes, hud, StyleEditorUI; + +add_task(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: "\u2018font-weight\u2019", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }, + { + text: "\u2018color\u2019", + category: CATEGORY_CSS, + severity: SEVERITY_WARNING, + }], + }).then(([error1Rule, error2Rule]) => { + let error1Msg = [...error1Rule.matched][0]; + let error2Msg = [...error2Rule.matched][0]; + nodes = [error1Msg.querySelector(".message-location .frame-link"), + error2Msg.querySelector(".message-location .frame-link")]; + ok(nodes[0], ".frame-link node for the first error"); + ok(nodes[1], ".frame-link 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; + deferred.resolve(panel); + }); + + EventUtils.sendMouseEvent({ type: "click" }, nodes[0].querySelector(".frame-link-filename")); + }); + + return deferred.promise; +} + +function onStyleEditorReady(panel) { + let deferred = promise.defer(); + + let win = panel.panelWindow; + ok(win, "Style Editor Window is defined"); + ok(StyleEditorUI, "Style Editor UI is defined"); + + function fireEvent(toolbox, href, line) { + toolbox.once("styleeditor-selected", function (evt) { + info(evt + " event fired"); + + checkStyleEditorForSheetAndLine(href, line - 1).then(deferred.resolve); + }); + + EventUtils.sendMouseEvent({ type: "click" }, nodes[1].querySelector(".frame-link-filename")); + } + + waitForFocus(function () { + info("style editor window focused"); + + let href = nodes[0].getAttribute("data-url"); + let line = nodes[0].getAttribute("data-line"); + 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); + + href = nodes[1].getAttribute("data-url"); + line = nodes[1].getAttribute("data-line"); + ok(line, "found source line"); + + toolbox.selectTool("webconsole").then(function () { + info("webconsole selected"); + fireEvent(toolbox, href, line); + }); + }); + }, win); + + return deferred.promise; +} + +function checkStyleEditorForSheetAndLine(href, line) { + let foundEditor = null; + for (let editor of StyleEditorUI.editors) { + if (editor.styleSheet.href == href) { + foundEditor = editor; + break; + } + } + + ok(foundEditor, "found style editor for " + href); + return performLineCheck(foundEditor, line); +} + +function performLineCheck(editor, line) { + let deferred = promise.defer(); + + function checkForCorrectState() { + is(editor.sourceEditor.getCursor().line, line, + "correct line is selected"); + is(StyleEditorUI.selectedStyleSheetIndex, editor.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(() => { + editor.getSourceEditor().then(() => { + // Get out of the editor's source-editor-load event loop. + executeSoon(checkForCorrectState); + }); + }); + + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js new file mode 100644 index 0000000000..b040e63144 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js @@ -0,0 +1,227 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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"; + +var jsterm, inputNode; + +add_task(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.getInputValue(), "jsterm.getInputValue() 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(jsterm.getInputValue() + lineValues[i]); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }); + } + let inputValue = jsterm.getInputValue(); + 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(jsterm.getInputValue().slice(inputNode.selectionStart), expectedStringAfterCarat, + "up arrow from end of multiline"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + is(jsterm.getInputValue().slice(inputNode.selectionStart), "", + "down arrow from within multiline"); + + // navigate up through input lines + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(jsterm.getInputValue().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(jsterm.getInputValue().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(jsterm.getInputValue(), 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(jsterm.getInputValue().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(jsterm.getInputValue().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(jsterm.getInputValue().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 j = 0; j < match.length; j++) { + 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(jsterm.getInputValue(), values[i - 1], + "ctrl-p updates inputNode from backwards history values[" + i - 1 + "]"); + } + let inputValue = jsterm.getInputValue(); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(inputNode.selectionStart, 0, + "ctrl-p at beginning of history moves caret location to beginning " + + "of line"); + is(jsterm.getInputValue(), 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(jsterm.getInputValue(), 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(!jsterm.getInputValue(), "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(jsterm.getInputValue(), inputValue, + "ctrl-p from end of multi-line does not trigger history"); + + EventUtils.synthesizeKey("a", { ctrlKey: true }); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + is(jsterm.getInputValue(), values[values.length - 1], + "ctrl-p from start of multi-line triggers history"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js new file mode 100644 index 0000000000..ccbcb3bf30 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + testEditedInputHistory(hud); +}); + +function testEditedInputHistory(HUD) { + let jsterm = HUD.jsterm; + let inputNode = jsterm.inputNode; + ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty"); + is(inputNode.selectionStart, 0); + is(inputNode.selectionEnd, 0); + + jsterm.setInputValue('"first item"'); + EventUtils.synthesizeKey("VK_UP", {}); + is(jsterm.getInputValue(), '"first item"', "null test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(jsterm.getInputValue(), '"first item"', "null test history down"); + + jsterm.execute(); + is(jsterm.getInputValue(), "", "cleared input line after submit"); + + jsterm.setInputValue('"editing input 1"'); + EventUtils.synthesizeKey("VK_UP", {}); + is(jsterm.getInputValue(), '"first item"', "test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(jsterm.getInputValue(), '"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(jsterm.getInputValue(), '"second item"', "test history up"); + EventUtils.synthesizeKey("VK_UP", {}); + is(jsterm.getInputValue(), '"first item"', "test history up"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(jsterm.getInputValue(), '"second item"', "test history down"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(jsterm.getInputValue(), '"editing input 2"', + "test history down restores new in-progress input again"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js new file mode 100644 index 0000000000..0524e1c4cf --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js @@ -0,0 +1,42 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" + + "test/test-bug-837351-security-errors.html"; + +add_task(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 \u201chttp://example.com/\u201d", + 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/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js new file mode 100644 index 0000000000..8062ffeecc --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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.dirxml works as intended. + +"use strict"; + +const TEST_URI = `data:text/html;charset=utf-8,Web Console test for bug 922212: + Add console.dirxml`; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + // Should work like console.log(window) + hud.jsterm.execute("console.dirxml(window)"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.dirxml(window) output:", + text: /Window \u2192/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + hud.jsterm.clearOutput(); + + hud.jsterm.execute("console.dirxml(document.body)"); + + // Should work like console.log(document.body); + [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.dirxml(document.body) output:", + text: "<body>", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + let msg = [...result.matched][0]; + yield checkLinkToInspector(true, msg); +}); + diff --git a/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js new file mode 100644 index 0000000000..fd5c4d29ac --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js @@ -0,0 +1,114 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the cached autocomplete results are used when the new +// user input is a subset of the existing completion results. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test cached autocompletion " + + "results"; + +var jsterm; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js new file mode 100644 index 0000000000..480c609408 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js @@ -0,0 +1,115 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the cd() jsterm helper function works as expected. See bug 609872. + +"use strict"; + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/devtools/client/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/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js new file mode 100644 index 0000000000..ca08d1a0f0 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the Web Console shows weak crypto warnings (SHA-1 Certificate) + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Web Console weak crypto " + + "warnings test"; +const TEST_URI_PATH = "/browser/devtools/client/webconsole/test/" + + "test-certificate-messages.html"; + +var gWebconsoleTests = [ + {url: "https://sha1ee.example.com" + TEST_URI_PATH, + name: "SHA1 warning displayed successfully", + warning: ["SHA-1"], nowarning: ["SSL 3.0", "RC4"]}, + {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"; + +var gHud = undefined, gContentBrowser; +var 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 () { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url); + }); + } else { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url); + } +} + +function onLoad() { + gContentBrowser.removeEventListener("load", onLoad, true); + + 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/devtools/client/webconsole/test/browser_webconsole_chrome.js b/devtools/client/webconsole/test/browser_webconsole_chrome.js new file mode 100644 index 0000000000..2513d1df55 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_chrome.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/test/browser_webconsole_clear_method.js b/devtools/client/webconsole/test/browser_webconsole_clear_method.js new file mode 100644 index 0000000000..a4702980eb --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_clear_method.js @@ -0,0 +1,131 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that calls to console.clear from a script delete the messages +// previously logged. + +"use strict"; + +add_task(function* () { + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-clear.html"; + + yield loadTab(TEST_URI); + let hud = yield openConsole(); + ok(hud, "Web Console opened"); + + info("Check the console.clear() done on page load has been processed."); + yield waitForLog("Console was cleared", hud); + ok(hud.outputNode.textContent.includes("Console was cleared"), + "console.clear() message is displayed"); + ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed"); + ok(!hud.outputNode.textContent.includes("log2"), "log2 not displayed"); + + info("Logging two messages log3, log4"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.log("log3"); + content.wrappedJSObject.console.log("log4"); + }); + + yield waitForLog("log3", hud); + yield waitForLog("log4", hud); + + ok(hud.outputNode.textContent.includes("Console was cleared"), + "console.clear() message is still displayed"); + ok(hud.outputNode.textContent.includes("log3"), "log3 is displayed"); + ok(hud.outputNode.textContent.includes("log4"), "log4 is displayed"); + + info("Open the variables view sidebar for 'objFromPage'"); + yield openSidebar("objFromPage", { a: 1 }, hud); + let sidebarClosed = hud.jsterm.once("sidebar-closed"); + + info("Call console.clear from the page"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.clear(); + }); + + // Cannot wait for "Console was cleared" here because such a message is + // already present and would yield immediately. + info("Wait for variables view sidebar to be closed after console.clear()"); + yield sidebarClosed; + + ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed"); + ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed"); + ok(hud.outputNode.textContent.includes("Console was cleared"), + "console.clear() message is still displayed"); + is(hud.outputNode.textContent.split("Console was cleared").length, 2, + "console.clear() message is only displayed once"); + + info("Logging one messages log5"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + content.wrappedJSObject.console.log("log5"); + }); + yield waitForLog("log5", hud); + + info("Close and reopen the webconsole."); + yield closeConsole(gBrowser.selectedTab); + hud = yield openConsole(); + yield waitForLog("Console was cleared", hud); + + ok(hud.outputNode.textContent.includes("Console was cleared"), + "console.clear() message is still displayed"); + ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed"); + ok(!hud.outputNode.textContent.includes("log2"), "log1 not displayed"); + ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed"); + ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed"); + ok(hud.outputNode.textContent.includes("log5"), "log5 still displayed"); +}); + +/** + * Wait for a single message to be logged in the provided webconsole instance + * with the category CATEGORY_WEBDEV and the SEVERITY_LOG severity. + * + * @param {String} message + * The expected messaged. + * @param {WebConsole} webconsole + * WebConsole instance in which the message should be logged. + */ +function* waitForLog(message, webconsole, options) { + yield waitForMessages({ + webconsole: webconsole, + messages: [{ + text: message, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); +} + +/** + * Open the variables view sidebar for the object with the provided name objName + * and wait for the expected object is displayed in the variables view. + * + * @param {String} objName + * The name of the object to open in the sidebar. + * @param {Object} expectedObj + * The properties that should be displayed in the variables view. + * @param {WebConsole} webconsole + * WebConsole instance in which the message should be logged. + * + */ +function* openSidebar(objName, expectedObj, webconsole) { + let msg = yield webconsole.jsterm.execute(objName); + ok(msg, "output message found"); + + let anchor = msg.querySelector("a"); + let body = msg.querySelector(".message-body"); + ok(anchor, "object anchor"); + ok(body, "message body"); + + yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, webconsole.iframeWindow); + + let vviewVar = yield webconsole.jsterm.once("variablesview-fetched"); + let vview = vviewVar._variablesView; + ok(vview, "variables view object exists"); + + yield findVariableViewProperties(vviewVar, [ + expectedObj, + ], { webconsole: webconsole }); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js new file mode 100644 index 0000000000..57d81fd051 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js @@ -0,0 +1,103 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// 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 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", + }, + + // 9: Shortened URL in an array + { + input: "['http://example.com/abcdefghijabcdefghij some other text']", + output: "Array [ \"http://example.com/abcdefghijabcdef\u2026\" ]", + printOutput: "http://example.com/abcdefghijabcdefghij some other text", + expectedTab: "http://example.com/abcdefghijabcdefghij", + getClickableNode: (msg) => msg.querySelectorAll("a")[1], + }, + + // 10: Shortened URL in an object + { + input: "{test: 'http://example.com/abcdefghijabcdefghij some other text'}", + output: "Object { test: \"http://example.com/abcdefghijabcdef\u2026\" }", + printOutput: "[object Object]", + evalOutput: "http://example.com/abcdefghijabcdefghij some other text", + noClick: true, + consoleLogClick: true, + expectedTab: "http://example.com/abcdefghijabcdefghij", + getClickableNode: (msg) => msg.querySelectorAll("a")[1], + }, + +]; + +const url = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS"; + +add_task(function* () { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let hud = yield openConsole(); + yield checkOutputForInputs(hud, inputTests); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js new file mode 100644 index 0000000000..6a29d61aab --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js @@ -0,0 +1,100 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that inspecting a closure in the variables view sidebar works when +// execution is paused. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-closures.html"; + +var gWebConsole, gJSTerm, gVariablesView; + +// Force the old debugger UI since it's directly used (see Bug 1301705) +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +function test() { + registerCleanupFunction(() => { + gWebConsole = gJSTerm = gVariablesView = null; + }); + + function fetchScopes(hud, toolbox, panelWin, deferred) { + panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => { + ok(true, "Scopes were fetched"); + toolbox.selectTool("webconsole").then(() => consoleOpened(hud)); + deferred.resolve(); + }); + } + + loadTab(TEST_URI).then(() => { + openConsole().then((hud) => { + openDebugger().then(({ toolbox, panelWin }) => { + let deferred = promise.defer(); + fetchScopes(hud, toolbox, panelWin, deferred); + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let button = content.document.querySelector("button"); + ok(button, "button element found"); + button.click(); + }); + + 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(results) { + let clickable = results[0].clickableElements[0]; + ok(clickable, "clickable object found"); + + gJSTerm.once("variablesview-fetched", onGetNameFetch); + let contextMenu = + gWebConsole.iframeWindow.document.getElementById("output-contextmenu"); + waitForContextMenu(contextMenu, clickable, () => { + let openInVarView = contextMenu.querySelector("#menu_openInVarView"); + ok(openInVarView.disabled === false, + "the \"Open In Variables View\" context menu item should be clickable"); + // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX + openInVarView.click(); + }); +} + +function onGetNameFetch(evt, view) { + gVariablesView = view._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(view, [ + { name: /_pfactory/, value: "" }, + ], { webconsole: gWebConsole }).then(onExpandClosure); +} + +function onExpandClosure(results) { + let prop = results[0].matchedProp; + ok(prop, "matched the name property in the variables view"); + + gVariablesView.window.focus(); + gJSTerm.once("sidebar-closed", finishTest); + EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_column_numbers.js b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js new file mode 100644 index 0000000000..8407e34d56 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js @@ -0,0 +1,46 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + + // Check if console provides the right column number alongside line number + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-column.html"; + +var 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:7", "10:39", "11:9", "12:11", "13:9", "14:7"]; + + for (let i = 0, len = messages.length; i < len; i++) { + let msg = messages[i].textContent; + + is(msg.includes(expected[i]), true, "Found expected line:column of " + + expected[i]); + } + + finishTest(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_completion.js b/devtools/client/webconsole/test/browser_webconsole_completion.js new file mode 100644 index 0000000000..ee0c6809ef --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_completion.js @@ -0,0 +1,106 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 code completion works properly. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test code completion"; + +var jsterm; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js new file mode 100644 index 0000000000..f8f02aa15c --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js @@ -0,0 +1,85 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/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", + // 21,22,23 + line: /\b2[123]\b/, + }, { + 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/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js new file mode 100644 index 0000000000..310d4fc8b9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/webconsole/test/browser_webconsole_console_extras.js b/devtools/client/webconsole/test/browser_webconsole_console_extras.js new file mode 100644 index 0000000000..078e331196 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_extras.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js new file mode 100644 index 0000000000..317337543c --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the basic console.log()-style APIs and filtering work. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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(method, hud, outputNode) { + let console = content.console; + + hud.jsterm.clearOutput(); + + console[method]("foo-bar-baz"); + console[method]("baar-baz"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo-bar-baz", + }, { + text: "baar-baz", + }], + }); + + setStringFilter("foo", hud); + + is(outputNode.querySelectorAll(".filtered-by-string").length, 1, + "1 hidden " + method + " node via string filtering"); + + hud.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[method]("foo-bar-baz"); + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo-bar-baz", + }], + }); + + setStringFilter("", hud); + let filter; + switch (method) { + case "debug": + filter = "log"; + break; + case "exception": + filter = "error"; + break; + default: + filter = method; + break; + } + + hud.setFilterState(filter, false); + + is(outputNode.querySelectorAll(".filtered-by-type").length, 1, + "1 message hidden for " + method + " (logging turned off)"); + + hud.setFilterState(filter, true); + + is(outputNode.querySelectorAll(".message:not(.filtered-by-type)").length, 1, + "1 message shown for " + method + " (logging turned on)"); + + hud.jsterm.clearOutput(); + + // test for multiple arguments. + console[method]("foo", "bar"); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo bar", + category: CATEGORY_WEBDEV, + }], + }); +} + +function setStringFilter(value, hud) { + hud.ui.filterBox.value = value; + hud.ui.adjustVisibilityOnSearchStringChange(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js new file mode 100644 index 0000000000..9575721c30 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the basic console.log()-style APIs and filtering work for +// sharedWorkers + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-workers.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "foo-bar-shared-worker" + }], + }); + + hud.setFilterState("sharedworkers", false); + + is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 1, + "1 message hidden for sharedworkers (logging turned off)"); + + hud.setFilterState("sharedworkers", true); + + is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 0, + "1 message shown for sharedworkers (logging turned on)"); + + hud.setFilterState("sharedworkers", false); + + hud.jsterm.clearOutput(true); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js new file mode 100644 index 0000000000..10c3ff7a56 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/" + + "webconsole/test/test-console-trace-async.html"; + +add_task(function* runTest() { + // Async stacks aren't on by default in all builds + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["javascript.options.asyncstack", true] + ]}, resolve); + }); + + let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + let hud = yield openConsole(tab); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.trace output", + consoleTrace: { + file: "test-console-trace-async.html", + fn: "inner", + }, + }], + }); + + let node = [...result.matched][0]; + ok(node, "found trace log node"); + ok(node.textContent.includes("console.trace()"), + "trace log node includes console.trace()"); + ok(node.textContent.includes("promise callback"), + "trace log node includes promise callback"); + ok(node.textContent.includes("setTimeout handler"), + "trace log node includes setTimeout handler"); + + // The expected stack trace object. + let stacktrace = [ + { + columnNumber: 3, + filename: TEST_URI, + functionName: "inner", + language: 2, + lineNumber: 9 + }, + { + asyncCause: "promise callback", + columnNumber: 3, + filename: TEST_URI, + functionName: "time1", + language: 2, + lineNumber: 13, + }, + { + asyncCause: "setTimeout handler", + columnNumber: 1, + filename: TEST_URI, + functionName: "", + language: 2, + lineNumber: 18, + } + ]; + + let obj = node._messageObject; + ok(obj, "console.trace message object"); + ok(obj._stacktrace, "found stacktrace object"); + is(obj._stacktrace.toSource(), stacktrace.toSource(), + "stacktrace is correct"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js new file mode 100644 index 0000000000..e1c6f966e4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +function test() { + const TEST_URI = "http://example.com/browser/devtools/client/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); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js new file mode 100644 index 0000000000..8451ec762b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js @@ -0,0 +1,51 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the "Open in Variables View" context menu item is enabled +// only for objects. + +"use strict"; + +const TEST_URI = `data:text/html,<script> + console.log("foo"); + console.log("foo", window); +</script>`; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2, + text: /foo/ + }], + }); + + let [msgWithText, msgWithObj] = [...result.matched]; + ok(msgWithText && msgWithObj, "Two messages should have appeared"); + + let contextMenu = hud.iframeWindow.document + .getElementById("output-contextmenu"); + let openInVarViewItem = contextMenu.querySelector("#menu_openInVarView"); + let obj = msgWithObj.querySelector(".cm-variable"); + let text = msgWithText.querySelector(".console-string"); + + yield waitForContextMenu(contextMenu, obj, () => { + ok(openInVarViewItem.disabled === false, "The \"Open In Variables View\" " + + "context menu item should be available for objects"); + }, () => { + ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " + + "context menu item should be disabled on popup hiding"); + }); + + yield waitForContextMenu(contextMenu, text, () => { + ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " + + "context menu item should be disabled for texts"); + }); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js new file mode 100644 index 0000000000..4508101ee6 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js @@ -0,0 +1,66 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the "Store as global variable" context menu item feature. +// It should be work, and be enabled only for objects + +"use strict"; + +const TEST_URI = `data:text/html,<script> + window.bar = { baz: 1 }; + console.log("foo"); + console.log("foo", window.bar); +</script>`; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2, + text: /foo/ + }], + }); + + let [msgWithText, msgWithObj] = [...result.matched]; + ok(msgWithText && msgWithObj, "Two messages should have appeared"); + + let contextMenu = hud.iframeWindow.document + .getElementById("output-contextmenu"); + let storeAsGlobalItem = contextMenu.querySelector("#menu_storeAsGlobal"); + let obj = msgWithObj.querySelector(".cm-variable"); + let text = msgWithText.querySelector(".console-string"); + let onceInputSet = hud.jsterm.once("set-input-value"); + + info("Waiting for context menu on the object"); + yield waitForContextMenu(contextMenu, obj, () => { + ok(storeAsGlobalItem.disabled === false, "The \"Store as global\" " + + "context menu item should be available for objects"); + storeAsGlobalItem.click(); + }, () => { + ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " + + "context menu item should be disabled on popup hiding"); + }); + + info("Waiting for context menu on the text node"); + yield waitForContextMenu(contextMenu, text, () => { + ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " + + "context menu item should be disabled for texts"); + }); + + info("Waiting for input to be set"); + yield onceInputSet; + + is(hud.jsterm.getInputValue(), "temp0", "Input was set"); + let executedResult = yield hud.jsterm.execute(); + + ok(executedResult.textContent.includes("{ baz: 1 }"), + "Correct variable assigned into console"); + +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_count.js b/devtools/client/webconsole/test/browser_webconsole_count.js new file mode 100644 index 0000000000..abb31a08d5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_count.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 console.count() counts as expected. See bug 922208. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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); + + BrowserTestUtils.synthesizeMouseAtCenter("#local", {}, gBrowser.selectedBrowser); + 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(); + + BrowserTestUtils.synthesizeMouseAtCenter("#external", {}, gBrowser.selectedBrowser); + 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/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js new file mode 100644 index 0000000000..61ac68208a --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/" + + "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); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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(event) { + urlNode.removeEventListener("click", onClick); + ok(event.defaultPrevented, "The default action was prevented."); + + deferred.resolve(); + }); + + EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2}, + hud.iframeWindow); + + yield deferred.promise; + } +} diff --git a/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js new file mode 100644 index 0000000000..5cedfbad5b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js @@ -0,0 +1,104 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 that the console receive exceptions include a stackframe. +// See bug 1184172. + +// On e10s, the exception is triggered in child process +// and is ignored by test harness +if (!Services.appinfo.browserTabsRemoteAutostart) { + SimpleTest.ignoreAllUncaughtExceptions(); +} + +function test() { + let hud; + + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-exception-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: 21, + }, { + file: TEST_FILE, + fn: "secondCall", + line: 17, + }, { + file: TEST_FILE, + fn: "firstCall", + line: 12, + }]; + + let results = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "nonExistingMethodCall is not defined", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: stack, + }, { + text: "SyntaxError: 'buggy;selector' is not a valid selector", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: [{ + file: TEST_FILE, + fn: "domAPI", + line: 25, + }, { + file: TEST_FILE, + fn: "onLoadDomAPI", + line: 33, + } + ] + }, { + text: "DOMException", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + collapsible: true, + stacktrace: [{ + file: TEST_FILE, + fn: "domException", + line: 29, + }, { + file: TEST_FILE, + fn: "onLoadDomException", + line: 36, + }, + + ] + }], + }); + + let elem = [...results[0].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/devtools/client/webconsole/test/browser_webconsole_execution_scope.js b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js new file mode 100644 index 0000000000..78865c9b25 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 commands run by the user are executed in content space. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js new file mode 100644 index 0000000000..192387e8ad --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 for the message timestamps option: check if the preference toggles the +// display of messages in the console output. See bug 722267. + +"use strict"; + +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"; +var hud; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js new file mode 100644 index 0000000000..e210bd81a3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the filter button context menu logic works correctly. + +"use strict"; + +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/devtools/client/webconsole/test/browser_webconsole_for_of.js b/devtools/client/webconsole/test/browser_webconsole_for_of.js new file mode 100644 index 0000000000..83d3aaa3db --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_for_of.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// A for-of loop in Web Console code can loop over a content NodeList. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-for-of.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + yield testForOf(hud); +}); + +function testForOf(hud) { + let deferred = promise.defer(); + + let jsterm = hud.jsterm; + jsterm.execute("{ let _nodes = []; for (let x of document.body.childNodes) { if (x.nodeType === 1) { _nodes.push(x.tagName); } } _nodes.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/devtools/client/webconsole/test/browser_webconsole_history.js b/devtools/client/webconsole/test/browser_webconsole_history.js new file mode 100644 index 0000000000..5ae709a4b7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_history.js @@ -0,0 +1,62 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the console history feature accessed via the up and down arrow keys. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +// Constants used for defining the direction of JSTerm input history navigation. +const HISTORY_BACK = -1; +const HISTORY_FORWARD = 1; + +add_task(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 (let item of executeList) { + input.value = item; + yield jsterm.execute(); + } + + for (let x = executeList.length - 1; x != -1; x--) { + jsterm.historyPeruse(HISTORY_BACK); + is(input.value, executeList[x], "check history previous idx:" + x); + } + + 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 (let 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/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js new file mode 100644 index 0000000000..3ee33669de --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js @@ -0,0 +1,126 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 errors about invalid HPKP security headers are logged to the web +// console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console HPKP invalid " + + "header test"; +const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" + + "test/test_hpkp-invalid-headers.sjs"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" + + "Public_Key_Pinning" + DOCS_GA_PARAMS; +const NON_BUILTIN_ROOT_PREF = "security.cert_pinning.process_headers_from_" + + "non_builtin_roots"; + +add_task(function* () { + registerCleanupFunction(() => { + Services.prefs.clearUserPref(NON_BUILTIN_ROOT_PREF); + }); + + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield* checkForMessage({ + url: SJS_URL + "?badSyntax", + name: "Could not parse header error displayed successfully", + text: "Public-Key-Pins: The site specified a header that could not be " + + "parsed successfully." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?noMaxAge", + name: "No max-age error displayed successfully", + text: "Public-Key-Pins: The site specified a header that did not include " + + "a \u2018max-age\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?invalidIncludeSubDomains", + name: "Invalid includeSubDomains error displayed successfully", + text: "Public-Key-Pins: The site specified a header that included an " + + "invalid \u2018includeSubDomains\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?invalidMaxAge", + name: "Invalid max-age error displayed successfully", + text: "Public-Key-Pins: The site specified a header that included an " + + "invalid \u2018max-age\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?multipleIncludeSubDomains", + name: "Multiple includeSubDomains error displayed successfully", + text: "Public-Key-Pins: The site specified a header that included " + + "multiple \u2018includeSubDomains\u2019 directives." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?multipleMaxAge", + name: "Multiple max-age error displayed successfully", + text: "Public-Key-Pins: The site specified a header that included " + + "multiple \u2018max-age\u2019 directives." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?multipleReportURIs", + name: "Multiple report-uri error displayed successfully", + text: "Public-Key-Pins: The site specified a header that included " + + "multiple \u2018report-uri\u2019 directives." + }, hud); + + // The root used for mochitests is not built-in, so set the relevant pref to + // true to have the PKP implementation return more specific errors. + Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, true); + + yield* checkForMessage({ + url: SJS_URL + "?pinsetDoesNotMatch", + name: "Non-matching pinset error displayed successfully", + text: "Public-Key-Pins: The site specified a header that did not include " + + "a matching pin." + }, hud); + + Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, false); + + yield* checkForMessage({ + url: SJS_URL + "?pinsetDoesNotMatch", + name: "Non-built-in root error displayed successfully", + text: "Public-Key-Pins: The certificate used by the site was not issued " + + "by a certificate in the default root certificate store. To " + + "prevent accidental breakage, the specified header was ignored." + }, hud); +}); + +function* checkForMessage(curTest, hud) { + hud.jsterm.clearOutput(); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: curTest.name, + text: curTest.text, + 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/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js new file mode 100644 index 0000000000..19cedefdb2 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js @@ -0,0 +1,92 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 errors about invalid HSTS security headers are logged +// to the web console. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console HSTS invalid " + + "header test"; +const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" + + "test/test_hsts-invalid-headers.sjs"; +const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" + + "HTTP_strict_transport_security" + DOCS_GA_PARAMS; + +add_task(function* () { + yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + yield* checkForMessage({ + url: SJS_URL + "?badSyntax", + name: "Could not parse header error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that could " + + "not be parsed successfully." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?noMaxAge", + name: "No max-age error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that did " + + "not include a \u2018max-age\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?invalidIncludeSubDomains", + name: "Invalid includeSubDomains error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that " + + "included an invalid \u2018includeSubDomains\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?invalidMaxAge", + name: "Invalid max-age error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that " + + "included an invalid \u2018max-age\u2019 directive." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?multipleIncludeSubDomains", + name: "Multiple includeSubDomains error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that " + + "included multiple \u2018includeSubDomains\u2019 directives." + }, hud); + + yield* checkForMessage({ + url: SJS_URL + "?multipleMaxAge", + name: "Multiple max-age error displayed successfully", + text: "Strict-Transport-Security: The site specified a header that " + + "included multiple \u2018max-age\u2019 directives." + }, hud); +}); + +function* checkForMessage(curTest, hud) { + hud.jsterm.clearOutput(); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: curTest.name, + text: curTest.text, + 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/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js new file mode 100644 index 0000000000..2d7fda7f5f --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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"; + +add_task(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 openInspector(); + hud = yield openConsole(); + + is(hud.jsterm.inputNode.hasAttribute("focused"), true, + "inputNode should be focused"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js new file mode 100644 index 0000000000..f79ba386fc --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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: "XMLDocument", + 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/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js new file mode 100644 index 0000000000..7d45059fc7 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js @@ -0,0 +1,55 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the input box expands as the user types long lines. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_jsterm.js b/devtools/client/webconsole/test/browser_webconsole_jsterm.js new file mode 100644 index 0000000000..221c96fa65 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_jsterm.js @@ -0,0 +1,195 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +var jsterm; + +add_task(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"); + + // check that thrown strings produce error messages, + // and the message text matches that of a stringified error object + // bug 1099071 + jsterm.clearOutput(); + yield jsterm.execute("throw '';"); + yield checkResult((node) => { + return node.closest(".message").getAttribute("severity") === "error" && + node.textContent === new Error("").toString(); + }, "thrown empty string generates error message"); + + jsterm.clearOutput(); + yield jsterm.execute("throw 'tomatoes';"); + yield checkResult((node) => { + return node.closest(".message").getAttribute("severity") === "error" && + node.textContent === new Error("tomatoes").toString(); + }, "thrown non-empty string generates error message"); + + jsterm.clearOutput(); + yield jsterm.execute("throw { foo: 'bar' };"); + yield checkResult((node) => { + return node.closest(".message").getAttribute("severity") === "error" && + node.textContent === Object.prototype.toString(); + }, "thrown object generates error message"); + + // check that errors with entires in errordocs.js display links + // alongside their messages. + const ErrorDocs = require("devtools/server/actors/errordocs"); + + const ErrorDocStatements = { + "JSMSG_BAD_RADIX": "(42).toString(0);", + "JSMSG_BAD_ARRAY_LENGTH": "([]).length = -1", + "JSMSG_NEGATIVE_REPETITION_COUNT": "'abc'.repeat(-1);", + "JSMSG_BAD_FORMAL": "var f = Function('x y', 'return x + y;');", + "JSMSG_PRECISION_RANGE": "77.1234.toExponential(-1);", + }; + + for (let errorMessageName of Object.keys(ErrorDocStatements)) { + let title = ErrorDocs.GetURL({ errorMessageName }).split("?")[0]; + + jsterm.clearOutput(); + yield jsterm.execute(ErrorDocStatements[errorMessageName]); + yield checkResult((node) => { + return node.parentNode.getElementsByTagName("a")[0].title == title; + }, `error links to ${title}`); + } + + // Ensure that dom errors, with error numbers outside of the range + // of valid js.msg errors, don't cause crashes (bug 1270721). + yield jsterm.execute("new Request('',{redirect:'foo'})"); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js new file mode 100644 index 0000000000..1dbfa80d9e --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the message type filter checkboxes work. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + for (let i = 0; i < 50; i++) { + content.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/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js new file mode 100644 index 0000000000..d41d5cf2e8 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js @@ -0,0 +1,96 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the text filter box works. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + hud.jsterm.clearOutput(); + + ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + for (let i = 0; i < 50; i++) { + content.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, value) { + hud.ui.filterBox.value = value; + hud.ui.adjustVisibilityOnSearchStringChange(); +} + diff --git a/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js new file mode 100644 index 0000000000..d5059485f1 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the text filter box works to filter based on filenames +// where the logs were generated. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-bug_923281_console_log_filter.html"; + +var hud; + +add_task(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(value) { + hud.ui.filterBox.value = value; + hud.ui.adjustVisibilityOnSearchStringChange(); +} + diff --git a/devtools/client/webconsole/test/browser_webconsole_message_node_id.js b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js new file mode 100644 index 0000000000..bec6577405 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js @@ -0,0 +1,28 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_multiline_input.js b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js new file mode 100644 index 0000000000..7285c21279 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js @@ -0,0 +1,70 @@ +/* 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 waits for more input instead of evaluating +// when valid, but incomplete, statements are present upon pressing enter +// -or- when the user ends a line with shift + enter. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +let SHOULD_ENTER_MULTILINE = [ + {input: "function foo() {" }, + {input: "var a = 1," }, + {input: "var a = 1;", shiftKey: true }, + {input: "function foo() { }", shiftKey: true }, + {input: "function" }, + {input: "(x) =>" }, + {input: "let b = {" }, + {input: "let a = [" }, + {input: "{" }, + {input: "{ bob: 3343," }, + {input: "function x(y=" }, + {input: "Array.from(" }, + // shift + enter creates a new line despite parse errors + {input: "{2,}", shiftKey: true }, +]; +let SHOULD_EXECUTE = [ + {input: "function foo() { }" }, + {input: "var a = 1;" }, + {input: "function foo() { var a = 1; }" }, + {input: '"asdf"' }, + {input: "99 + 3" }, + {input: "1, 2, 3" }, + // errors + {input: "function f(x) { let y = 1, }" }, + {input: "function f(x=,) {" }, + {input: "{2,}" }, +]; + +add_task(function* () { + let { tab, browser } = yield loadTab(TEST_URI); + let hud = yield openConsole(); + let inputNode = hud.jsterm.inputNode; + + for (let test of SHOULD_ENTER_MULTILINE) { + hud.jsterm.setInputValue(test.input); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey }); + let inputValue = hud.jsterm.getInputValue(); + is(inputNode.selectionStart, inputNode.selectionEnd, + "selection is collapsed"); + is(inputNode.selectionStart, inputValue.length, + "caret at end of multiline input"); + let inputWithNewline = test.input + "\n"; + is(inputValue, inputWithNewline, "Input value is correct"); + } + + for (let test of SHOULD_EXECUTE) { + hud.jsterm.setInputValue(test.input); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey }); + let inputValue = hud.jsterm.getInputValue(); + is(inputNode.selectionStart, 0, "selection starts/ends at 0"); + is(inputNode.selectionEnd, 0, "selection starts/ends at 0"); + is(inputValue, "", "Input value is cleared"); + } + +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging.js b/devtools/client/webconsole/test/browser_webconsole_netlogging.js new file mode 100644 index 0000000000..63730c9b4e --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_netlogging.js @@ -0,0 +1,139 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 response logging for different request types. + +"use strict"; + +// This test runs very slowly on linux32 debug - bug 1269977 +requestLongerTimeout(2); + +const TEST_NETWORK_REQUEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/" + + "test-network-request.html"; + +const TEST_DATA_JSON_CONTENT = + '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; + +const PAGE_REQUEST_PREDICATE = + ({ request }) => request.url.endsWith("test-network-request.html"); + +const TEST_DATA_REQUEST_PREDICATE = + ({ request }) => request.url.endsWith("test-data.json"); + +add_task(function* testPageLoad() { + // Enable logging in the UI. Not needed to pass test but makes it easier + // to debug interactively. + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": + [["devtools.webconsole.filter.networkinfo", true] + ]}, resolve); + }); + + let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE); + let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI); + let request = yield finishedRequest; + + ok(request, "Page load was logged"); + + let client = hud.ui.webConsoleClient; + let args = [request.actor]; + const postData = yield getPacket(client, "getRequestPostData", args); + const responseContent = yield getPacket(client, "getResponseContent", args); + + is(request.request.url, TEST_NETWORK_REQUEST_URI, + "Logged network entry is page load"); + is(request.request.method, "GET", "Method is correct"); + ok(!postData.postData.text, "No request body was stored"); + ok(!postData.postDataDiscarded, + "Request body was not discarded"); + is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0, + "Response body's beginning is okay"); + + yield closeTabAndToolbox(); +}); + +add_task(function* testXhrGet() { + let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI); + + let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE); + content.wrappedJSObject.testXhrGet(); + let request = yield finishedRequest; + + ok(request, "testXhrGet() was logged"); + + let client = hud.ui.webConsoleClient; + let args = [request.actor]; + const postData = yield getPacket(client, "getRequestPostData", args); + const responseContent = yield getPacket(client, "getResponseContent", args); + + is(request.request.method, "GET", "Method is correct"); + ok(!postData.postData.text, "No request body was sent"); + ok(!postData.postDataDiscarded, + "Request body was not discarded"); + is(responseContent.content.text, TEST_DATA_JSON_CONTENT, + "Response is correct"); + + yield closeTabAndToolbox(); +}); + +add_task(function* testXhrPost() { + let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI); + + let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE); + content.wrappedJSObject.testXhrPost(); + let request = yield finishedRequest; + + ok(request, "testXhrPost() was logged"); + + let client = hud.ui.webConsoleClient; + let args = [request.actor]; + const postData = yield getPacket(client, "getRequestPostData", args); + const responseContent = yield getPacket(client, "getResponseContent", args); + + is(request.request.method, "POST", "Method is correct"); + is(postData.postData.text, "Hello world!", "Request body was logged"); + is(responseContent.content.text, TEST_DATA_JSON_CONTENT, + "Response is correct"); + + yield closeTabAndToolbox(); +}); + +add_task(function* testFormSubmission() { + let pageLoadRequestFinished = waitForFinishedRequest(PAGE_REQUEST_PREDICATE); + let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI); + + info("Waiting for the page load to be finished."); + yield pageLoadRequestFinished; + + // The form POSTs to the page URL but over https (page over http). + let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE); + ContentTask.spawn(gBrowser.selectedBrowser, { }, `function() + { + let form = content.document.querySelector("form"); + form.submit(); + }`); + let request = yield finishedRequest; + + ok(request, "testFormSubmission() was logged"); + + let client = hud.ui.webConsoleClient; + let args = [request.actor]; + const postData = yield getPacket(client, "getRequestPostData", args); + const responseContent = yield getPacket(client, "getResponseContent", args); + + is(request.request.method, "POST", "Method is correct"); + isnot(postData.postData.text + .indexOf("Content-Type: application/x-www-form-urlencoded"), -1, + "Content-Type is correct"); + isnot(postData.postData.text + .indexOf("Content-Length: 20"), -1, "Content-length is correct"); + isnot(postData.postData.text + .indexOf("name=foo+bar&age=144"), -1, "Form data is correct"); + is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0, + "Response body's beginning is okay"); + + yield closeTabAndToolbox(); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js new file mode 100644 index 0000000000..c6fa12401b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.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/ */ + +// 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/devtools/client/" + + "webconsole/test/test-network.html" + "?_date=" + + Date.now(); + +add_task(function* () { + yield loadTab("data:text/html;charset=utf-8,Web Console basic network " + + "logging test"); + let hud = yield openConsole(); + + yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, 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/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js new file mode 100644 index 0000000000..b44b49453e --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.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/ */ + +// Tests that network log messages bring up the network panel. + +"use strict"; + +const TEST_NETWORK_REQUEST_URI = + "http://example.com/browser/devtools/client/webconsole/test/" + + "test-network-request.html"; + +add_task(function* () { + let finishedRequest = waitForFinishedRequest(({ request }) => { + return request.url.endsWith("test-network-request.html"); + }); + + const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI); + let request = yield finishedRequest; + + yield hud.ui.openNetworkPanel(request.actor); + let toolbox = gDevTools.getToolbox(hud.target); + is(toolbox.currentToolId, "netmonitor", "Network panel was opened"); + let panel = toolbox.getCurrentPanel(); + let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem; + is(selected.attachment.method, request.request.method, + "The correct request is selected"); + is(selected.attachment.url, request.request.url, + "The correct request is definitely selected"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js new file mode 100644 index 0000000000..265bc7c002 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 network log messages bring up the network panel and select the +// right request even if it was previously filtered off. + +"use strict"; + +const TEST_FILE_URI = + "http://example.com/browser/devtools/client/webconsole/test/" + + "test-network.html"; +const TEST_URI = "data:text/html;charset=utf8,<p>test file URI"; + +var hud; + +add_task(function* () { + let Actions = require("devtools/client/netmonitor/actions/index"); + + let requests = []; + let { browser } = yield loadTab(TEST_URI); + + yield pushPrefEnv(); + hud = yield openConsole(); + hud.jsterm.clearOutput(); + + HUDService.lastFinishedRequest.callback = request => requests.push(request); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_FILE_URI); + yield loaded; + + yield testMessages(); + let htmlRequest = requests.find(e => e.request.url.endsWith("html")); + ok(htmlRequest, "htmlRequest was a html"); + + yield hud.ui.openNetworkPanel(htmlRequest.actor); + let toolbox = gDevTools.getToolbox(hud.target); + is(toolbox.currentToolId, "netmonitor", "Network panel was opened"); + + let panel = toolbox.getCurrentPanel(); + let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem; + is(selected.attachment.method, htmlRequest.request.method, + "The correct request is selected"); + is(selected.attachment.url, htmlRequest.request.url, + "The correct request is definitely selected"); + + // Filter out the HTML request. + panel.panelWin.gStore.dispatch(Actions.toggleFilterType("js")); + + yield toolbox.selectTool("webconsole"); + is(toolbox.currentToolId, "webconsole", "Web console was selected"); + yield hud.ui.openNetworkPanel(htmlRequest.actor); + + panel.panelWin.NetMonitorView.RequestsMenu.selectedItem; + is(selected.attachment.method, htmlRequest.request.method, + "The correct request is selected"); + is(selected.attachment.url, htmlRequest.request.url, + "The correct request is definitely selected"); + + // All tests are done. Shutdown. + HUDService.lastFinishedRequest.callback = null; + htmlRequest = browser = requests = 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: "testscript.js", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); +} + +function pushPrefEnv() { + let deferred = promise.defer(); + let options = { + set: [["devtools.webconsole.filter.networkinfo", true]] + }; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} diff --git a/devtools/client/webconsole/test/browser_webconsole_notifications.js b/devtools/client/webconsole/test/browser_webconsole_notifications.js new file mode 100644 index 0000000000..4bda9192f9 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_notifications.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " + + "notifications"; + +add_task(function* () { + yield loadTab(TEST_URI); + + let consoleOpened = promise.defer(); + let gotEvents = waitForEvents(consoleOpened.promise); + yield openConsole().then(() => { + consoleOpened.resolve(); + }); + + yield gotEvents; +}); + +function waitForEvents(onConsoleOpened) { + let deferred = promise.defer(); + + function webConsoleCreated(id) { + Services.obs.removeObserver(observer, "web-console-created"); + ok(HUDService.getHudReferenceById(id), "We have a hud reference"); + content.wrappedJSObject.console.log("adding a log message"); + } + + function webConsoleDestroyed(id) { + Services.obs.removeObserver(observer, "web-console-destroyed"); + ok(!HUDService.getHudReferenceById(id), "We do not have a hud reference"); + executeSoon(deferred.resolve); + } + + function webConsoleMessage(id, nodeID) { + Services.obs.removeObserver(observer, "web-console-message-created"); + ok(id, "we have a console ID"); + is(typeof nodeID, "string", "message node id is a string"); + onConsoleOpened.then(closeConsole); + } + + let observer = { + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function observe(subject, topic, data) { + subject = subject.QueryInterface(Ci.nsISupportsString); + + switch (topic) { + case "web-console-created": + webConsoleCreated(subject.data); + break; + case "web-console-destroyed": + webConsoleDestroyed(subject.data); + break; + case "web-console-message-created": + webConsoleMessage(subject, data); + 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/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js new file mode 100644 index 0000000000..ae11305de4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 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/devtools/client/" + + "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/devtools/client/webconsole/test/browser_webconsole_output_01.js b/devtools/client/webconsole/test/browser_webconsole_output_01.js new file mode 100644 index 0000000000..c75577ea77 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_01.js @@ -0,0 +1,122 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// + +"use strict"; + +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for various types of objects. + +const TEST_URI = "data:text/html;charset=utf8,test for console output - 01"; + +var {DebuggerServer} = require("devtools/server/main"); + +var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a"); +var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + +var inputTests = [ + // 0 + { + input: "'hello \\nfrom \\rthe \\\"string world!'", + output: "\"hello \nfrom \rthe \"string world!\"", + consoleOutput: "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\"", + consoleOutput: "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165", + }, + + // 2 + { + input: "'" + longString + "'", + output: '"' + initialString + "\"[\u2026]", + consoleOutput: initialString + "[\u2026]", + printOutput: initialString, + }, + + // 3 + { + input: "''", + output: '""', + consoleOutput: "", + printOutput: '""', + }, + + // 4 + { + input: "0", + output: "0", + }, + + // 5 + { + input: "'0'", + output: '"0"', + consoleOutput: "0", + }, + + // 6 + { + input: "42", + output: "42", + }, + + // 7 + { + input: "'42'", + output: '"42"', + consoleOutput: "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); + + 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/devtools/client/webconsole/test/browser_webconsole_output_02.js b/devtools/client/webconsole/test/browser_webconsole_output_02.js new file mode 100644 index 0000000000..8018669a9f --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_02.js @@ -0,0 +1,183 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the webconsole output for various types of objects. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-output-02.html"; + +var 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; }", + suppressClick: true + }, + + // 2 - named function + { + input: "window.testfn1", + output: "function testfn1()", + printOutput: "function testfn1() { return 42; }", + suppressClick: true + }, + + // 3 - anonymous function, but spidermonkey gives us an inferred name. + { + input: "testobj1.testfn2", + output: "function testobj1.testfn2()", + printOutput: "function () { return 42; }", + suppressClick: true + }, + + // 4 - named function with custom display name + { + input: "window.testfn3", + output: "function testfn3DisplayName()", + printOutput: "function testfn3() { return 42; }", + suppressClick: true + }, + + // 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: "1,287,8651,40983,8754", + 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]", + }, + + // 15 - WeakSet + { + input: "window.weakset", + // Need a regexp because the order may vary. + output: new RegExp("WeakSet \\[ (String, <head>|<head>, String) \\]"), + printOutput: "[object WeakSet]", + inspectable: true, + variablesViewLabel: "WeakSet[2]", + }, + + // 16 - WeakMap + { + input: "window.weakmap", + // Need a regexp because the order may vary. + output: new RegExp("WeakMap { (String: 23, HTMLCollection\\[2\\]: Object|HTMLCollection\\[2\\]: Object, String: 23) }"), + printOutput: "[object WeakMap]", + inspectable: true, + variablesViewLabel: "WeakMap[2]", + }, +]; + +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/devtools/client/webconsole/test/browser_webconsole_output_03.js b/devtools/client/webconsole/test/browser_webconsole_output_03.js new file mode 100644 index 0000000000..bd77c2a4df --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_03.js @@ -0,0 +1,168 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the webconsole output for various types of objects. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-output-03.html"; + +var 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/devtools/client/webconsole/test/browser_webconsole_output_04.js b/devtools/client/webconsole/test/browser_webconsole_output_04.js new file mode 100644 index 0000000000..d829594a75 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_04.js @@ -0,0 +1,129 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// + +"use strict"; + +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for various types of objects. + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-output-04.html"; + +var 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: "'foo;()bar!' is not a valid selector"`, + printOutput: `"SyntaxError: 'foo;()bar!' is not a valid selector"`, + 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/devtools/client/webconsole/test/browser_webconsole_output_05.js b/devtools/client/webconsole/test/browser_webconsole_output_05.js new file mode 100644 index 0000000000..53bfd768c3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_05.js @@ -0,0 +1,177 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the webconsole output for various types of objects. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,test for console output - 05"; +const {ELLIPSIS} = require("devtools/shared/l10n"); + +// March, 1960: The first implementation of Lisp. From Wikipedia: +// +// > Lisp was first implemented by Steve Russell on an IBM 704 computer. Russell +// > had read McCarthy's paper, and realized (to McCarthy's surprise) that the +// > Lisp eval function could be implemented in machine code. The result was a +// > working Lisp interpreter which could be used to run Lisp programs, or more +// > properly, 'evaluate Lisp expressions.' +var testDate = -310435200000; + +var 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: "Boolean { false }", + printOutput: "false", + inspectable: true, + variablesViewLabel: "Boolean { false }" + }, + + // 5 + { + input: "new Date(" + testDate + ")", + output: "Date " + (new Date(testDate)).toISOString(), + printOutput: (new Date(testDate)).toString(), + inspectable: true, + }, + + // 6 + { + input: "new Date('test')", + output: "Invalid Date", + printOutput: "Invalid Date", + inspectable: true, + variablesViewLabel: "Invalid Date", + }, + + // 7 + { + input: "Date.prototype", + output: /Object \{.*\}/, + printOutput: "Invalid Date", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 8 + { + input: "new Number(43)", + output: "Number { 43 }", + printOutput: "43", + inspectable: true, + variablesViewLabel: "Number { 43 }" + }, + + // 9 + { + input: "new String('hello')", + output: /String { "hello", 6 more.* }/, + printOutput: "hello", + inspectable: true, + variablesViewLabel: "String" + }, + + // 10 + { + input: "(function () { var s = new String('hello'); s.whatever = 23; " + + " return s;})()", + output: /String { "hello", whatever: 23, 6 more.* }/, + printOutput: "hello", + inspectable: true, + variablesViewLabel: "String" + }, + + // 11 + { + input: "(function () { var s = new String('hello'); s[8] = 'x'; " + + " return s;})()", + output: /String { "hello", 8: "x", 6 more.* }/, + printOutput: "hello", + inspectable: true, + variablesViewLabel: "String" + }, + + // 12 + { + // 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" + }, + + // 13 + { + 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" + }, + + // 14 + { + 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: '[ <1 empty slot>, "this is supposed to be a very long ' + ELLIPSIS + + '", "a shorter string", 100 ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[4]" + }, + + // 15 + { + input: "new Proxy({a:1},[1,2,3])", + output: 'Proxy { <target>: Object, <handler>: Array[3] }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Proxy" + } +]; + +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 = testDate = null; + finishTest(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_output_06.js b/devtools/client/webconsole/test/browser_webconsole_output_06.js new file mode 100644 index 0000000000..ad69b39086 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_06.js @@ -0,0 +1,283 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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} = require("devtools/shared/l10n"); + +const testStrIn = "SHOW\\nALL\\nOF\\nTHIS\\nON\\nA\\nSINGLE" + + "\\nLINE ONLY. ESCAPE ALL NEWLINE"; +const testStrOut = "SHOW ALL OF THIS ON A SINGLE LINE O" + ELLIPSIS; + +var 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: '["' + testStrIn + '", "' + testStrIn + '", "' + testStrIn + '"]', + output: 'Array [ "' + testStrOut + '", "' + testStrOut + '", "' + + testStrOut + '" ]', + inspectable: true, + 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\n" + + "LINE ONLY. ESCAPE ALL NEWLINE", + variablesViewLabel: "Array[3]" + }, + + // 13 + { + input: '({0: "a", 1: "b"})', + output: 'Object [ "a", "b" ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[2]", + }, + + // 14 + { + input: '({0: "a", 42: "b"})', + output: '[ "a", <9 empty slots>, 33 more\u2026 ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[43]", + }, + + // 15 + { + input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' + + '7: "h", 8: "i", 9: "j", 10: "k", 11: "l"})', + output: 'Object [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", ' + + "2 more\u2026 ]", + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[12]", + }, + + // 16 + { + input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' + + '7: "h", 8: "i", 9: "j", 10: "k", 11: "l", m: "n"})', + output: 'Object { 0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", ' + + '6: "g", 7: "h", 8: "i", 9: "j", 3 more\u2026 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 17 + { + input: '({" ": "a"})', + output: 'Object { : "a" }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 18 + { + input: '({})', + output: 'Object { }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 19 + { + input: '({length: 0})', + output: 'Object [ ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[0]", + }, + + // 20 + { + input: '({length: 1})', + output: '[ <1 empty slot> ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[1]", + }, + + // 21 + { + input: '({0: "a", 1: "b", length: 1})', + output: 'Object { 0: "a", 1: "b", length: 1 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 22 + { + input: '({0: "a", 1: "b", length: 2})', + output: 'Object [ "a", "b" ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[2]", + }, + + // 23 + { + input: '({0: "a", 1: "b", length: 3})', + output: '[ "a", "b", <1 empty slot> ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[3]", + }, + + // 24 + { + input: '({0: "a", 2: "b", length: 2})', + output: 'Object { 0: "a", 2: "b", length: 2 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 25 + { + input: '({0: "a", 2: "b", length: 3})', + output: '[ "a", <1 empty slot>, "b" ]', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object[3]", + }, + + // 26 + { + input: '({0: "a", b: "b", length: 1})', + output: 'Object { 0: "a", b: "b", length: 1 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 27 + { + input: '({0: "a", b: "b", length: 2})', + output: 'Object { 0: "a", b: "b", length: 2 }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, + + // 28 + { + input: '({42: "a"})', + output: 'Object { 42: "a" }', + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object", + }, +]; + +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/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js new file mode 100644 index 0000000000..22de843f95 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js @@ -0,0 +1,72 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 multiple messages are copied into the clipboard and that they are +// separated by new lines. See bug 916997. + +"use strict"; + +add_task(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/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js new file mode 100644 index 0000000000..097eb3b370 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js @@ -0,0 +1,122 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejections should be fixed. + +"use strict"; + +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/devtools/client/webconsole/" + + "test/test-console-output-dom-elements.html"; + +var inputTests = [ + { + input: "testBodyNode()", + output: '<body class="body-class" id="body-id">', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocumentElement()", + output: '<html dir="ltr" lang="en-US">', + 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 [ <p>, <p#lots-of-attributes>, <iframe>, " + + "<div.some.classname.here.with.more.classnames.here>, " + + "<svg>, <clipPath>, <rect>, <script> ]", + printOutput: "[object NodeList]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testNodeInIframe()", + output: "<p>", + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testLotsOfAttributes()", + output: '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' + + 'h="" i="" j="" k="" l="" m="" n="">', + 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/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js new file mode 100644 index 0000000000..51fe89e012 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js @@ -0,0 +1,66 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the inspector links in the webconsole output for DOM Nodes actually +// open the inspector and select the right node. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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">', + displayName: "p", + attrs: [{name: "some-attribute", value: "some-value"}] + }, + { + input: "testBodyNode()", + output: '<body class="body-class" id="body-id">', + displayName: "body", + attrs: [ + { + name: "class", value: "body-class" + }, + { + name: "id", value: "body-id" + } + ] + }, + { + input: "testNodeInIframe()", + output: "<p>", + displayName: "p", + attrs: [] + }, + { + input: "testDocumentElement()", + output: '<html dir="ltr" lang="en-US">', + displayName: "html", + attrs: [ + { + name: "dir", + value: "ltr" + }, + { + name: "lang", + value: "en-US" + } + ] + } +]; + +function test() { + Task.spawn(function* () { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkDomElementHighlightingForInputs(hud, TEST_DATA); + }).then(finishTest); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js new file mode 100644 index 0000000000..b5dd125d19 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js @@ -0,0 +1,70 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 inspector links in webconsole outputs for DOM Nodes highlight +// the actual DOM Nodes on hover + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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"); + 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/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js new file mode 100644 index 0000000000..c7eb949022 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js @@ -0,0 +1,113 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 inspector links in the webconsole output for DOM Nodes do not try +// to highlight or select nodes once they have been detached + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/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: "testSvgNode()", + output: '<clipPath>' + }, + { + input: "testBodyNode()", + output: '<body class="body-class" id="body-id">' + }, + { + input: "testNodeInIframe()", + output: "<p>" + }, + { + input: "testDocumentElement()", + output: '<html dir="ltr" lang="en-US">' + } +]; + +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/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js new file mode 100644 index 0000000000..9d35ef984d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 inspector links in the webconsole output for namespaced elements +// actually open the inspector and select the right node. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath id="clip"> + <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +const TEST_DATA = [ + { + input: 'document.querySelector("clipPath")', + output: '<svg:clipPath id="clip">', + displayName: "svg:clipPath" + }, + { + input: 'document.querySelector("circle")', + output: '<svg:circle cx="0" cy="0" r="5">', + displayName: "svg:circle" + }, +]; + +function test() { + Task.spawn(function* () { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkDomElementHighlightingForInputs(hud, TEST_DATA); + }).then(finishTest); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_output_events.js b/devtools/client/webconsole/test/browser_webconsole_output_events.js new file mode 100644 index 0000000000..9bd04bfc7c --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_events.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. + +"use strict"; + +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null"); + +// Test the webconsole output for DOM events. + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-output-events.html"; + +add_task(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, + }], + }); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_output_order.js b/devtools/client/webconsole/test/browser_webconsole_output_order.js new file mode 100644 index 0000000000..66fa74cb0a --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_order.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 any output created from calls to the console API comes before the +// echoed JavaScript. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console.html"; + +add_task(function* () { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let jsterm = hud.jsterm; + + jsterm.clearOutput(); + jsterm.execute("console.log('foo', 'bar');"); + + let [functionCall, consoleMessage, result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "console.log('foo', 'bar');", + category: CATEGORY_INPUT, + }, + { + text: "foo bar", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "undefined", + category: CATEGORY_OUTPUT, + }] + }); + + let fncallNode = [...functionCall.matched][0]; + let consoleMessageNode = [...consoleMessage.matched][0]; + let resultNode = [...result.matched][0]; + is(fncallNode.nextElementSibling, consoleMessageNode, + "console.log() is followed by 'foo' 'bar'"); + is(consoleMessageNode.nextElementSibling, resultNode, + "'foo' 'bar' is followed by undefined"); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_output_regexp.js b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js new file mode 100644 index 0000000000..2d6e767e9d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the webconsole output for various types of objects. + +"use strict"; + +const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/test-console-output-regexp.html"; + +var inputTests = [ + // 0 + { + input: "/foo/igym", + output: "/foo/gimy", + printOutput: "Error: source called", + inspectable: true, + }, +]; + +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/devtools/client/webconsole/test/browser_webconsole_output_table.js b/devtools/client/webconsole/test/browser_webconsole_output_table.js new file mode 100644 index 0000000000..372afb28d5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_output_table.js @@ -0,0 +1,199 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/devtools/client/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" } + }, + { + command: "console.table(weakset)", + data: [ + { _value: "String" }, + { _value: "String" }, + ], + columns: { _index: "(iteration index)", _value: "Values" }, + couldBeOutOfOrder: true, + }, + { + command: "console.table(weakmap)", + data: [ + { _key: "String", _value: "\"oh no\"" }, + { _key: "String", _value: "23" }, + ], + columns: { _index: "(iteration index)", _key: "Key", _value: "Values" }, + couldBeOutOfOrder: true, + }, +]; + +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 entryResult = {}; + + for (let key of Object.keys(entries)) { + // If the results can be out of order, then ignore _index. + if (!testdata.couldBeOutOfOrder || key !== "_index") { + entryResult[key] = entries[key] instanceof HTMLElement ? + entries[key].textContent : entries[key]; + } + } + + return entryResult; + }); + + if (testdata.couldBeOutOfOrder) { + data = data.map(e => e.toSource()).sort().join(","); + let expected = testdata.data.map(e => e.toSource()).sort().join(","); + is(data, expected, "table data is correct"); + } else { + 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/devtools/client/webconsole/test/browser_webconsole_promise.js b/devtools/client/webconsole/test/browser_webconsole_promise.js new file mode 100644 index 0000000000..59cd287cad --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_promise.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Bug 1148759 - Test the webconsole can display promises inside objects. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,test for console and promises"; + +var inputTests = [ + // 0 + { + input: "({ x: Promise.resolve() })", + output: "Object { x: Promise }", + printOutput: "[object Object]", + inspectable: true, + variablesViewLabel: "Object" + }, +]; + +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() { + finishTest(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_property_provider.js b/devtools/client/webconsole/test/browser_webconsole_property_provider.js new file mode 100644 index 0000000000..0c9b4c4e34 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_property_provider.js @@ -0,0 +1,46 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 the property provider, which is part of the code completion +// infrastructure. + +"use strict"; + +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 {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider"); + + let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + tmp.addDebuggerToGlobal(tmp); + let dbg = new tmp.Debugger(); + let dbgWindow = dbg.addDebuggee(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. + let 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'"); + let matches = completion.matches; + matches.sort(); + ok(matches[0] == "location", "the first match is 'location'"); + ok(matches[1] == "locationbar", "the second match is 'locationbar'"); + + dbg.removeDebuggee(content); + finishTest(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_reflow.js b/devtools/client/webconsole/test/browser_webconsole_reflow.js new file mode 100644 index 0000000000..86caa10e05 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_reflow.js @@ -0,0 +1,33 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " + + "reflow activity"; + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + function onReflowListenersReady() { + 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/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js new file mode 100644 index 0000000000..566af8d422 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf8,<p>test Scratchpad panel " + + "linking</p>"; + +var { Tools } = require("devtools/client/definitions"); +var { isTargetSupported } = Tools.scratchpad; + +function pushPrefEnv() { + let deferred = promise.defer(); + let options = {"set": + [["devtools.scratchpad.enabled", true] + ]}; + SpecialPowers.pushPrefEnv(options, deferred.resolve); + return deferred.promise; +} + +add_task(function* () { + waitForExplicitFinish(); + + yield pushPrefEnv(); + + 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(".message-location .frame-link-filename"); + + 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.win.parent, + "Scratchpad's toolbox is focused"); + + Tools.scratchpad.isTargetSupported = isTargetSupported; + finish(); + }); + + EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow); +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js new file mode 100644 index 0000000000..779d80376b --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Ensure that [Learn More] links appear alongside any errors listed +// in "errordocs.js". Note: this only tests script execution. + +"use strict"; + +const ErrorDocs = require("devtools/server/actors/errordocs"); + +function makeURIData(script) { + return `data:text/html;charset=utf8,<script>${script}</script>`; +} + +const TestData = [ + { + jsmsg: "JSMSG_READ_ONLY", + script: "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;", + isException: true, + }, + { + jsmsg: "JSMSG_STMT_AFTER_RETURN", + script: "function a() { return; 1 + 1; };", + isException: false, + } +]; + +add_task(function* () { + yield loadTab("data:text/html;charset=utf8,errordoc tests"); + + let hud = yield openConsole(); + + for (let i = 0; i < TestData.length; i++) { + yield testScriptError(hud, TestData[i]); + } +}); + +function* testScriptError(hud, testData) { + if (testData.isException === true) { + expectUncaughtException(); + } + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, makeURIData(testData.script)); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + category: CATEGORY_JS + } + ] + }); + + // grab the most current error doc URL + let url = ErrorDocs.GetURL({ errorMessageName: testData.jsmsg }); + + let hrefs = {}; + for (let link of hud.jsterm.outputNode.querySelectorAll("a")) { + hrefs[link.href] = true; + } + + ok(url in hrefs, `Expected a link to ${url}.`); + + hud.jsterm.clearOutput(); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js new file mode 100644 index 0000000000..43cb96bdc4 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Ensure non-toplevel security errors are displayed + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console subresource STS " + + "warning test"; +const TEST_DOC = "https://example.com/browser/devtools/client/webconsole/" + + "test/test_bug1092055_shouldwarn.html"; +const SAMPLE_MSG = "specified a header that could not be parsed successfully."; + +add_task(function* setup() { + yield SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]] + }); +}); + +add_task(function* () { + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + hud.jsterm.clearOutput(); + + let loaded = loadBrowser(browser); + BrowserTestUtils.loadURI(browser, 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/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js new file mode 100644 index 0000000000..b66d5afff2 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js @@ -0,0 +1,73 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf8,Test that the web console " + + "displays requests that have been recorded in the " + + "netmonitor, even if the console hadn't opened yet."; + +const TEST_FILE = "test-network-request.html"; +const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" + + "test/" + TEST_FILE; + +const NET_PREF = "devtools.webconsole.filter.networkinfo"; +Services.prefs.setBoolPref(NET_PREF, true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref(NET_PREF); +}); + +add_task(function* () { + let { tab, browser } = yield loadTab(TEST_URI); + + let target = TargetFactory.forTab(tab); + let toolbox = yield gDevTools.showToolbox(target, "netmonitor"); + info("Network panel is open."); + + yield loadDocument(browser); + info("Document loaded."); + + // Test that the request appears in the network panel. + testNetmonitor(toolbox); + + // Test that the request appears in the console. + let hud = yield openConsole(); + info("Web console is open"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "network message", + text: TEST_FILE, + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG + } + ] + }); +}); + +function loadDocument(browser) { + let deferred = promise.defer(); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve(); + }, true); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH); + + return deferred.promise; +} + +function testNetmonitor(toolbox) { + let monitor = toolbox.getCurrentPanel(); + let { RequestsMenu } = monitor.panelWin.NetMonitorView; + RequestsMenu.lazyUpdate = false; + is(RequestsMenu.itemCount, 1, "Network request appears in the network panel"); + + let item = RequestsMenu.getItemAtIndex(0); + is(item.attachment.method, "GET", "The attached method is correct."); + is(item.attachment.url, TEST_PATH, "The attached url is correct."); +} diff --git a/devtools/client/webconsole/test/browser_webconsole_split.js b/devtools/client/webconsole/test/browser_webconsole_split.js new file mode 100644 index 0000000000..0242d94b4c --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_split.js @@ -0,0 +1,268 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for splitting"; + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, runTest); +} + +function runTest() { + // Test is slow on Linux EC2 instances - Bug 962931 + requestLongerTimeout(2); + + let {Toolbox} = require("devtools/client/framework/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.win); + } + + 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("performance"); + yield openAndCheckPanel("netmonitor"); + + yield checkWebconsolePanelOpened(); + testBottomHost(); + }); + } + + function getCurrentUIState() { + let win = toolbox.win; + 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(() => { + 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(() => { + 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) { + 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/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js new file mode 100644 index 0000000000..f71efb99e5 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js @@ -0,0 +1,158 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + + 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.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.win; + contentWindow.focus(); + EventUtils.sendKey("ESCAPE", contentWindow); + + return result; + } + + function testHideSplitConsoleAfterEscape() { + let result = toolbox.once("split-console", () => { + ok(!toolbox.splitConsole, "Split console is hidden."); + }); + EventUtils.sendKey("ESCAPE", toolbox.win); + + 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.win); + + return result; + } + + function testHideAutoCompletePopupAfterEscape() { + let deferred = promise.defer(); + let popup = jsterm.autocompletePopup; + + popup.once("popup-closed", () => { + ok(!popup.isOpen, + "Auto complete popup is hidden."); + ok(toolbox.splitConsole, + "Split console is open after hiding the autocomplete popup."); + + deferred.resolve(); + }); + + EventUtils.sendKey("ESCAPE", toolbox.win); + + 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 onPopupShown = jsterm.autocompletePopup.once("popup-opened"); + + jsterm.focus(); + jsterm.setInputValue("document.location."); + EventUtils.sendKey("TAB", hud.iframeWindow); + + return onPopupShown; + } + + function finish() { + toolbox.destroy().then(() => { + toolbox = null; + hud = null; + jsterm = null; + hudMessages = null; + variablesView = null; + + finishTest(); + }); + } + } diff --git a/devtools/client/webconsole/test/browser_webconsole_split_focus.js b/devtools/client/webconsole/test/browser_webconsole_split_focus.js new file mode 100644 index 0000000000..ff65229c90 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_split_focus.js @@ -0,0 +1,66 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + + 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(); + + let activeElement = getActiveElement(inspector.panelDoc); + 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"); + activeElement = getActiveElement(inspector.panelDoc); + 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 finish() { + toolbox = TEST_URI = null; + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight"); + finishTest(); + } +} diff --git a/devtools/client/webconsole/test/browser_webconsole_split_persist.js b/devtools/client/webconsole/test/browser_webconsole_split_persist.js new file mode 100644 index 0000000000..e11bd48118 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + + 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.win; + 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/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js new file mode 100644 index 0000000000..a10acf9b2d --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that the webconsole works if the network monitor is first opened, then +// the user switches to the webconsole. See bug 970914. + +"use strict"; + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello"); + + 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/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js new file mode 100644 index 0000000000..c8f2200f9f --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Check that "use strict" JS errors generate errors, not warnings. + +"use strict"; + +add_task(function* () { + // On e10s, the exception is triggered in child process + // and is ignored by test harness + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + yield loadTab("data:text/html;charset=utf8,<script>'use strict';var arguments;</script>"); + + let hud = yield openConsole(); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "SyntaxError: 'arguments' can't be defined or assigned to in strict mode code", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + ], + }); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset=" + + "utf8,<script>'use strict';function f(a, a) {};</script>"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "SyntaxError: duplicate formal argument a", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + ], + }); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset=" + + "utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "TypeError: setting a property that has only a getter", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + ], + }); + + if (!Services.appinfo.browserTabsRemoteAutostart) { + expectUncaughtException(); + } + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, + "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>"); + + yield waitForMessages({ + webconsole: hud, + messages: [ + { + text: "ReferenceError: assignment to undeclared variable v", + category: CATEGORY_JS, + severity: SEVERITY_ERROR, + }, + ], + }); + + hud = null; +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js new file mode 100644 index 0000000000..eafeee18e3 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +// Load a page with tracking elements that get blocked and make sure that a +// 'learn more' link shows up in the webconsole. + +"use strict"; + +const TEST_URI = "http://tracking.example.org/browser/devtools/client/" + + "webconsole/test/test-trackingprotection-securityerrors.html"; +const LEARN_MORE_URI = "https://developer.mozilla.org/Firefox/Privacy/" + + "Tracking_Protection" + DOCS_GA_PARAMS; +const PREF = "privacy.trackingprotection.enabled"; + +const {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref(PREF); + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(function* testMessagesAppear() { + yield UrlClassifierTestUtils.addTestTrackers(); + Services.prefs.setBoolPref(PREF, true); + + let { browser } = yield loadTab(TEST_URI); + + let hud = yield openConsole(); + + let results = yield waitForMessages({ + webconsole: hud, + messages: [ + { + name: "Was blocked because tracking protection is enabled", + text: "The resource at \u201chttp://tracking.example.com/\u201d was " + + "blocked because tracking protection is enabled", + category: CATEGORY_SECURITY, + severity: SEVERITY_WARNING, + objects: true, + }, + ], + }); + + yield testClickOpenNewTab(hud, results[0]); +}); + +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/devtools/client/webconsole/test/browser_webconsole_view_source.js b/devtools/client/webconsole/test/browser_webconsole_view_source.js new file mode 100644 index 0000000000..a81b58acc1 --- /dev/null +++ b/devtools/client/webconsole/test/browser_webconsole_view_source.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 source URLs in the Web Console can be clicked to display the +// standard View Source window. As JS exceptions and console.log() messages always +// have their locations opened in Debugger, we need to test a security message in +// order to have it opened in the standard View Source window. + +"use strict"; + +const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" + + "test/test-mixedcontent-securityerrors.html"; + +add_task(function* () { + yield actuallyTest(); +}); + +add_task(function* () { + Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); + yield actuallyTest(); + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); +}); + +var actuallyTest = Task.async(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(null); + info("console opened"); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "Blocked loading mixed active content", + category: CATEGORY_SECURITY, + severity: SEVERITY_ERROR, + }], + }); + + let msg = [...result.matched][0]; + ok(msg, "error message"); + let locationNode = msg.querySelector(".message-location .frame-link-filename"); + ok(locationNode, "location node"); + + let onTabOpen = waitForTab(); + + EventUtils.sendMouseEvent({ type: "click" }, locationNode); + + let tab = yield onTabOpen; + ok(true, "the view source tab was opened in response to clicking the location node"); + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/webconsole/test/head.js b/devtools/client/webconsole/test/head.js new file mode 100644 index 0000000000..519cb78b05 --- /dev/null +++ b/devtools/client/webconsole/test/head.js @@ -0,0 +1,1844 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +/* import-globals-from ../../framework/test/shared-head.js */ +"use strict"; + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this); + +var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils"); +var {Messages} = require("devtools/client/webconsole/console-output"); +const asyncStorage = require("devtools/shared/async-storage"); +const HUDService = require("devtools/client/webconsole/hudservice"); + +// Services.prefs.setBoolPref("devtools.debugger.log", true); + +var 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; +const CATEGORY_SERVER = 7; + +// 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 = "devtools/client/locales/webconsole.properties"; +var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI); + +const DOCS_GA_PARAMS = "?utm_source=mozilla" + + "&utm_medium=firefox-console-errors" + + "&utm_campaign=default"; + +flags.testing = true; + +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) { + return BrowserTestUtils.browserLoaded(browser); +} + +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; +} + +/** + * Load the page and return the associated HUD. + * + * @param string uri + * The URI of the page to load. + * @param string consoleType [optional] + * The console type, either "browserConsole" or "webConsole". Defaults to + * "webConsole". + * @return object + * The HUD associated with the console + */ +function* loadPageAndGetHud(uri, consoleType) { + let { browser } = yield loadTab("data:text/html;charset=utf-8,Loading tab for tests"); + + let hud; + if (consoleType === "browserConsole") { + hud = yield HUDService.openBrowserConsoleOrFocus(); + } else { + hud = yield openConsole(); + } + + ok(hud, "Console was opened"); + + let loaded = loadBrowser(browser); + yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri); + yield loaded; + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: uri, + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }], + }); + + return hud; +} + +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} outputNode + * the HUD output node. + * @param {string} matchString + * the string you want to check if it exists in the output node. + * @param {string} msg + * the message describing the test + * @param {boolean} [onlyVisible=false] + * find only messages that are visible, not hidden by the filter. + * @param {boolean} [failIfFound=false] + * fail the test if the string is found in the output node. + * @param {string} cssClass [optional] + * find only messages with the given CSS class. + */ +function testLogEntry(outputNode, matchString, msg, onlyVisible, + failIfFound, cssClass) { + let selector = ".message"; + // Skip entries that are hidden by the filter. + if (onlyVisible) { + selector += ":not(.filtered-by-type):not(.filtered-by-string)"; + } + if (cssClass) { + selector += "." + aClass; + } + + let msgs = outputNode.querySelectorAll(selector); + let found = false; + for (let i = 0, n = msgs.length; i < n; i++) { + let message = msgs[i].textContent.indexOf(matchString); + if (message > -1) { + found = true; + break; + } + } + + is(found, !failIfFound, msg); +} + +/** + * A convenience method to call testLogEntry(). + * + * @param str string + * The string to find. + */ +function findLogEntry(str) { + testLogEntry(outputNode, str, "found " + str); +} + +/** + * Open the Web Console for the given tab. + * + * @param nsIDOMElement [tab] + * Optional tab element for which you want open the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [callback] + * 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. + */ +var openConsole = function (tab) { + let webconsoleOpened = promise.defer(); + let target = TargetFactory.forTab(tab || 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 [tab] + * Optional tab element for which you want close the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [callback] + * 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. + */ +var closeConsole = Task.async(function* (tab) { + let target = TargetFactory.forTab(tab || gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + if (toolbox) { + yield toolbox.destroy(); + } +}); + +/** + * Listen for a new tab to open and return a promise that resolves when one + * does and completes the load event. + * @return a promise that resolves to the tab object + */ +var waitForTab = Task.async(function* () { + info("Waiting for a tab to open"); + yield once(gBrowser.tabContainer, "TabOpen"); + let tab = gBrowser.selectedTab; + let browser = tab.linkedBrowser; + yield once(browser, "load", true); + info("The tab load completed"); + return tab; +}); + +/** + * 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 message + * The message element you want to display. + */ +function dumpMessageElement(message) { + let text = message.textContent; + let repeats = message.querySelector(".message-repeats"); + if (repeats) { + repeats = repeats.getAttribute("value"); + } + console.debug("id", message.getAttribute("id"), + "date", message.timestamp, + "class", message.className, + "category", message.category, + "severity", message.severity, + "repeats", repeats, + "clipboardText", message.clipboardText, + "text", text); +} + +var 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(); +}); + +// Always use the 'old' frontend for tests that rely on it +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); +}); + +registerCleanupFunction(function* () { + flags.testing = false; + + // Remove stored console commands in between tests + yield asyncStorage.removeItem("webConsoleHistory"); + + 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); + + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +waitForExplicitFinish(); + +/** + * Polls a given function waiting for it to become true. + * + * @param object options + * 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(options) { + let deferred = promise.defer(); + let start = Date.now(); + let timeout = options.timeout || 5000; + let {validator} = options; + + function wait() { + if ((Date.now() - start) > timeout) { + // Log the failure. + ok(false, "Timed out while waiting for: " + options.name); + deferred.reject(null); + return; + } + + if (validator(options)) { + ok(true, options.name); + deferred.resolve(null); + } else { + setTimeout(wait, 100); + } + } + + setTimeout(wait, 100); + + return deferred.promise; +} + +var openInspector = Task.async(function* (tab = gBrowser.selectedTab) { + let target = TargetFactory.forTab(tab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + return toolbox.getCurrentPanel(); +}); + +/** + * Find variables or properties in a VariablesView instance. + * + * @param object view + * The VariablesView instance. + * @param array rules + * 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 options + * 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(view, rules, options) { + // Initialize the search. + function init() { + // Separate out the rules that require expanding properties throughout the + // view. + let expandRules = []; + let filterRules = rules.filter((rule) => { + if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) { + expandRules.push(rule); + 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(filterRules, view, 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 rules array. + let returnResults = onAllRulesMatched.bind(null, rules); + + return promise.all(outstanding).then(lastStep).then(returnResults); + } + + function onMatch(prop, rule, matched) { + if (matched && !rule.matchedProp) { + rule.matchedProp = prop; + } + } + + function finder(rules, vars, promises) { + for (let [, prop] of vars) { + for (let rule of rules) { + let matcher = matchVariablesViewProperty(prop, rule, options); + promises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + + function processExpandRules(rules) { + let rule = rules.shift(); + if (!rule) { + return promise.resolve(null); + } + + let deferred = promise.defer(); + let expandOptions = { + rootVariable: view, + expandTo: rule.name, + webconsole: options.webconsole, + }; + + variablesViewExpandTo(expandOptions).then(function onSuccess(prop) { + let name = rule.name; + let lastName = name.split(".").pop(); + rule.name = lastName; + + let matched = matchVariablesViewProperty(prop, rule, options); + return matched.then(onMatch.bind(null, prop, rule)).then(function () { + rule.name = name; + }); + }, function onFailure() { + return promise.resolve(null); + }).then(processExpandRules.bind(null, rules)).then(function () { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function onAllRulesMatched(rules) { + for (let rule of rules) { + 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 rules; + } + + return init(); +} + +/** + * Check if a given Property object from the variables view matches the given + * rule. + * + * @param object prop + * The variable's view Property instance. + * @param object rule + * Rules for matching the property. See findVariableViewProperties() for + * details. + * @param object options + * 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(prop, rule, options) { + function resolve(result) { + return promise.resolve(result); + } + + if (rule.name) { + let match = rule.name instanceof RegExp ? + rule.name.test(prop.name) : + prop.name == rule.name; + if (!match) { + return resolve(false); + } + } + + if (rule.value) { + let displayValue = prop.displayValue; + if (prop.displayValueClassName == "token-string") { + displayValue = displayValue.substring(1, displayValue.length - 1); + } + + let match = rule.value instanceof RegExp ? + rule.value.test(displayValue) : + displayValue == rule.value; + if (!match) { + info("rule " + rule.name + " did not match value, expected '" + + rule.value + "', found '" + displayValue + "'"); + return resolve(false); + } + } + + if ("isGetter" in rule) { + let isGetter = !!(prop.getter && prop.get("get")); + if (rule.isGetter != isGetter) { + info("rule " + rule.name + " getter test failed"); + return resolve(false); + } + } + + if ("isGenerator" in rule) { + let isGenerator = prop.displayValue == "Generator"; + if (rule.isGenerator != isGenerator) { + info("rule " + rule.name + " generator test failed"); + return resolve(false); + } + } + + let outstanding = []; + + if ("isIterator" in rule) { + let isIterator = isVariableViewPropertyIterator(prop, options.webconsole); + outstanding.push(isIterator.then((result) => { + if (result != rule.isIterator) { + info("rule " + rule.name + " iterator test failed"); + } + return result == rule.isIterator; + })); + } + + outstanding.push(promise.resolve(true)); + + return promise.all(outstanding).then(function _onMatchDone(results) { + let ruleMatched = results.indexOf(false) == -1; + return resolve(ruleMatched); + }); +} + +/** + * Check if the given variables view property is an iterator. + * + * @param object prop + * The Property instance you want to check. + * @param object webConsole + * 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(prop, webConsole) { + if (prop.displayValue == "Iterator") { + return promise.resolve(true); + } + + let deferred = promise.defer(); + + variablesViewExpandTo({ + rootVariable: prop, + expandTo: "__proto__.__iterator__", + webconsole: webConsole, + }).then(function onSuccess() { + deferred.resolve(true); + }, function onFailure() { + deferred.resolve(false); + }); + + return deferred.promise; +} + +/** + * Recursively expand the variables view up to a given property. + * + * @param options + * 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(options) { + let root = options.rootVariable; + let expandTo = options.expandTo.split("."); + let jsterm = (options.webconsole || {}).jsterm; + let lastDeferred = promise.defer(); + + function fetch(prop) { + if (!prop.onexpand) { + ok(false, "property " + prop.name + " cannot be expanded: !onexpand"); + return promise.reject(prop); + } + + let deferred = promise.defer(); + + if (prop._fetched || !jsterm) { + executeSoon(function () { + deferred.resolve(prop); + }); + } else { + jsterm.once("variablesview-fetched", function _onFetchProp() { + executeSoon(() => deferred.resolve(prop)); + }); + } + + prop.expand(); + + return deferred.promise; + } + + function getNext(prop) { + let name = expandTo.shift(); + let newProp = prop.get(name); + + if (expandTo.length > 0) { + ok(newProp, "found property " + name); + if (newProp) { + fetch(newProp).then(getNext, fetchError); + } else { + lastDeferred.reject(prop); + } + } else if (newProp) { + lastDeferred.resolve(newProp); + } else { + lastDeferred.reject(prop); + } + } + + function fetchError(prop) { + lastDeferred.reject(prop); + } + + 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 options + * 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. + */ +var updateVariablesViewProperty = Task.async(function* (options) { + let view = options.property._variablesView; + view.window.focus(); + options.property.focus(); + + switch (options.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 options.string) { + EventUtils.synthesizeKey(c, {}, view.window); + } + + if (options.webconsole) { + options.webconsole.jsterm.once("variablesview-fetched") + .then((varView) => deferred.resolve(varView)); + } + + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); + + if (!options.webconsole) { + executeSoon(() => { + deferred.resolve(null); + }); + } + }); + + return deferred.promise; +}); + +/** + * Open the JavaScript debugger. + * + * @param object options + * 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(options = {}) { + if (!options.tab) { + options.tab = gBrowser.selectedTab; + } + + let deferred = promise.defer(); + + let target = TargetFactory.forTab(options.tab); + let toolbox = gDevTools.getToolbox(target); + let dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger"); + + gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(tool) { + let panel = tool.getCurrentPanel(); + let panelWin = panel.panelWin; + + panel._view.Variables.lazyEmpty = false; + + let resolveObject = { + target: target, + toolbox: tool, + panel: panel, + panelWin: panelWin, + }; + + if (dbgPanelAlreadyOpen) { + deferred.resolve(resolveObject); + } else { + panelWin.DebuggerController.waitForSourcesLoaded().then(() => { + deferred.resolve(resolveObject); + }); + } + }, function onFailure(reason) { + console.debug("failed to open the toolbox for 'jsdebugger'", reason); + deferred.reject(reason); + }); + + return deferred.promise; +} + +/** + * Returns true if the caret in the debugger editor is placed at the specified + * position. + * @param panel The debugger panel. + * @param {number} line The line number. + * @param {number} [col] The column number. + * @returns {boolean} + */ +function isDebuggerCaretPos(panel, line, col = 1) { + let editor = panel.panelWin.DebuggerView.editor; + let cursor = editor.getCursor(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor caret position: " + (cursor.line + 1) + ", " + + (cursor.ch + 1)); + return cursor.line == (line - 1) && cursor.ch == (col - 1); +} + +/** + * Wait for messages in the Web Console output. + * + * @param object options + * 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. + * - prefix: prefix text to check for in the prefix element. + * - 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(options) { + info("Waiting for messages..."); + + gPendingOutputTest++; + let webconsole = options.webconsole; + let rules = WebConsoleUtils.cloneObject(options.messages, true); + let rulesMatched = 0; + let listenerAdded = false; + let deferred = promise.defer(); + options.matchCondition = options.matchCondition || "all"; + + function checkText(rule, text) { + let result = false; + if (Array.isArray(rule)) { + result = rule.every((s) => checkText(s, text)); + } else if (typeof rule == "string") { + result = text.indexOf(rule) > -1; + } else if (rule instanceof RegExp) { + result = rule.test(text); + } else { + result = rule == text; + } + return result; + } + + function checkConsoleTable(rule, element) { + let elemText = element.textContent; + + if (!checkText("console.table():", elemText)) { + return false; + } + + rule.category = CATEGORY_WEBDEV; + rule.severity = SEVERITY_LOG; + rule.type = Messages.ConsoleTable; + + return true; + } + + function checkConsoleTrace(rule, element) { + let elemText = element.textContent; + let trace = rule.consoleTrace; + + if (!checkText("console.trace():", elemText)) { + return false; + } + + rule.category = CATEGORY_WEBDEV; + rule.severity = SEVERITY_LOG; + rule.type = Messages.ConsoleTrace; + + if (!rule.stacktrace && typeof trace == "object" && trace !== true) { + if (Array.isArray(trace)) { + rule.stacktrace = trace; + } else { + rule.stacktrace = [trace]; + } + } + + return true; + } + + function checkConsoleTime(rule, element) { + let elemText = element.textContent; + let time = rule.consoleTime; + + if (!checkText(time + ": timer started", elemText)) { + return false; + } + + rule.category = CATEGORY_WEBDEV; + rule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleTimeEnd(rule, element) { + let elemText = element.textContent; + let time = rule.consoleTimeEnd; + let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms"); + + if (!checkText(regex, elemText)) { + return false; + } + + rule.category = CATEGORY_WEBDEV; + rule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleDir(rule, element) { + if (!element.classList.contains("inlined-variables-view")) { + return false; + } + + let elemText = element.textContent; + if (!checkText(rule.consoleDir, elemText)) { + return false; + } + + let iframe = element.querySelector("iframe"); + if (!iframe) { + ok(false, "console.dir message has no iframe"); + return false; + } + + return true; + } + + function checkConsoleGroup(rule) { + if (!isNaN(parseInt(rule.consoleGroup, 10))) { + rule.groupDepth = rule.consoleGroup; + } + rule.category = CATEGORY_WEBDEV; + rule.severity = SEVERITY_LOG; + + return true; + } + + function checkSource(rule, element) { + let location = getRenderedSource(element); + if (!location) { + return false; + } + + if (!checkText(rule.source.url, location.url)) { + return false; + } + + if ("line" in rule.source && location.line != rule.source.line) { + return false; + } + + return true; + } + + function checkCollapsible(rule, element) { + let msg = element._messageObject; + if (!msg || !!msg.collapsible != rule.collapsible) { + return false; + } + + return true; + } + + function checkStacktrace(rule, element) { + let stack = rule.stacktrace; + let frames = element.querySelectorAll(".stacktrace > .stack-trace > .frame-link"); + 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 url = frame.getAttribute("data-url"); + if (!checkText(expected.file, url)) { + ok(false, "frame #" + i + " does not match file name: " + + expected.file + " != " + url); + displayErrorContext(rule, element); + return false; + } + } + + if (expected.fn) { + let fn = frame.querySelector(".frame-link-function-display-name").textContent; + if (!checkText(expected.fn, fn)) { + ok(false, "frame #" + i + " does not match the function name: " + + expected.fn + " != " + fn); + displayErrorContext(rule, element); + return false; + } + } + + if (expected.line) { + let line = frame.getAttribute("data-line"); + if (!checkText(expected.line, line)) { + ok(false, "frame #" + i + " does not match the line number: " + + expected.line + " != " + line); + displayErrorContext(rule, element); + return false; + } + } + } + + return true; + } + + function hasXhrLabel(element) { + let xhr = element.querySelector(".xhr"); + if (!xhr) { + return false; + } + return true; + } + + function checkMessage(rule, element) { + let elemText = element.textContent; + + if (rule.text && !checkText(rule.text, elemText)) { + return false; + } + + if (rule.noText && checkText(rule.noText, elemText)) { + return false; + } + + if (rule.consoleTable && !checkConsoleTable(rule, element)) { + return false; + } + + if (rule.consoleTrace && !checkConsoleTrace(rule, element)) { + return false; + } + + if (rule.consoleTime && !checkConsoleTime(rule, element)) { + return false; + } + + if (rule.consoleTimeEnd && !checkConsoleTimeEnd(rule, element)) { + return false; + } + + if (rule.consoleDir && !checkConsoleDir(rule, element)) { + return false; + } + + if (rule.consoleGroup && !checkConsoleGroup(rule, element)) { + return false; + } + + if (rule.source && !checkSource(rule, element)) { + return false; + } + + if ("collapsible" in rule && !checkCollapsible(rule, element)) { + return false; + } + + if (rule.isXhr && !hasXhrLabel(element)) { + return false; + } + + if (!rule.isXhr && hasXhrLabel(element)) { + return false; + } + + let partialMatch = !!(rule.consoleTrace || rule.consoleTime || + rule.consoleTimeEnd); + + // The rule tries to match the newer types of messages, based on their + // object constructor. + if (rule.type) { + if (!element._messageObject || + !(element._messageObject instanceof rule.type)) { + if (partialMatch) { + ok(false, "message type for rule: " + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + partialMatch = true; + } + + if ("category" in rule && element.category != rule.category) { + if (partialMatch) { + is(element.category, rule.category, + "message category for rule: " + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + + if ("severity" in rule && element.severity != rule.severity) { + if (partialMatch) { + is(element.severity, rule.severity, + "message severity for rule: " + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + + if (rule.text) { + partialMatch = true; + } + + if (rule.stacktrace && !checkStacktrace(rule, element)) { + if (partialMatch) { + ok(false, "failed to match stacktrace for rule: " + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + + if (rule.category == CATEGORY_NETWORK && "url" in rule && + !checkText(rule.url, element.url)) { + return false; + } + + if ("repeats" in rule) { + let repeats = element.querySelector(".message-repeats"); + if (!repeats || repeats.getAttribute("value") != rule.repeats) { + return false; + } + } + + if ("groupDepth" in rule) { + let indentNode = element.querySelector(".indent"); + let indent = (GROUP_INDENT * rule.groupDepth) + "px"; + if (!indentNode || indentNode.style.width != indent) { + is(indentNode.style.width, indent, + "group depth check failed for message rule: " + displayRule(rule)); + return false; + } + } + + if ("longString" in rule) { + let longStrings = element.querySelectorAll(".longStringEllipsis"); + if (rule.longString != !!longStrings[0]) { + if (partialMatch) { + is(!!longStrings[0], rule.longString, + "long string existence check failed for message rule: " + + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + rule.longStrings = longStrings; + } + + if ("objects" in rule) { + let clickables = element.querySelectorAll(".message-body a"); + if (rule.objects != !!clickables[0]) { + if (partialMatch) { + is(!!clickables[0], rule.objects, + "objects existence check failed for message rule: " + + displayRule(rule)); + displayErrorContext(rule, element); + } + return false; + } + rule.clickableElements = clickables; + } + + if ("prefix" in rule) { + let prefixNode = element.querySelector(".prefix"); + is(prefixNode && prefixNode.textContent, rule.prefix, "Check prefix"); + } + + let count = rule.count || 1; + if (!rule.matched) { + rule.matched = new Set(); + } + rule.matched.add(element); + + return rule.matched.size == count; + } + + function onMessagesAdded(event, newMessages) { + for (let msg of newMessages) { + let elem = msg.node; + let location = getRenderedSource(elem); + if (location && location.url) { + let url = location.url; + // Prevent recursion with the browser console and any potential + // messages coming from head.js. + if (url.indexOf("devtools/client/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 options.matchCondition == "all" && rulesMatched == rules.length || + options.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(rule) { + return rule.name || rule.text; + } + + function displayErrorContext(rule, element) { + console.log("error occured during rule " + displayRule(rule)); + console.log("while checking the following message"); + dumpMessageElement(element); + } + + 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(win, callback) { + Services.obs.addObserver(function observer(subject, topic) { + if (win == subject) { + Services.obs.removeObserver(observer, topic); + executeSoon(callback); + } + }, "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). + * + * - consoleOutput: string|RegExp, optional, expected consoleOutput + * If not provided consoleOuput = output; + * + * - 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"); + hud.jsterm.clearOutput(); + hud.jsterm.execute("console.log(" + entry.input + ")"); + + let consoleOutput = "consoleOutput" in entry ? + entry.consoleOutput : entry.output; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log() output: " + consoleOutput, + text: consoleOutput, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + let msg = [...result.matched][0]; + + if (entry.consoleLogClick) { + yield checkObjectClick(entry, msg); + } + + if (typeof entry.inspectorIcon == "boolean") { + info("Checking Inspector Link"); + yield checkLinkToInspector(entry.inspectorIcon, msg); + } + } + + function checkPrintOutput(entry) { + info("Printing"); + 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"); + hud.jsterm.clearOutput(); + hud.jsterm.execute(entry.input); + + let evalOutput = entry.evalOutput || entry.output; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "JS eval output: " + entry.evalOutput, + text: entry.evalOutput, + category: CATEGORY_OUTPUT, + }], + }); + + let msg = [...result.matched][0]; + if (!entry.noClick) { + yield checkObjectClick(entry, msg); + } + if (typeof entry.inspectorIcon == "boolean") { + info("Checking Inspector Link: " + entry.input); + yield checkLinkToInspector(entry.inspectorIcon, msg); + } + } + + function* checkObjectClick(entry, msg) { + info("Clicking"); + let body; + if (entry.getClickableNode) { + body = entry.getClickableNode(msg); + } else { + 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(); + + if (!entry.suppressClick) { + 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 onVariablesViewOpen(entry, {resolve, reject}, event, view, options) { + info("Variables view opened"); + 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); + + Task.spawn(function* () { + yield loadBrowser(browser); + let uri = yield ContentTask.spawn(browser, {}, function* () { + return content.location.href; + }); + ok(entry.expectedTab && entry.expectedTab == uri, + "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'"); + yield closeTab(tab); + }).then(resolve, reject); + } + + return Task.spawn(runner); +} + +/** + * Check the web console DOM element output for the given inputs. + * Each input is checked for the expected JS eval result. The JS eval result is + * also checked if it opens the inspector with the correct node selected on + * inspector icon 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, expected JS eval result. + * + * - displayName: string, expected NodeFront's displayName. + * + * - attr: Array, expected NodeFront's attributes + */ +function checkDomElementHighlightingForInputs(hud, inputs) { + function* runner() { + 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 inputs) { + let [result] = yield jsEval(data.input, {text: data.output}); + let {msg} = yield checkWidgetAndMessage(result); + yield checkNodeHighlight(toolbox, inspector, msg, data); + } + } + + function jsEval(input, message) { + info("Executing '" + input + "' in the web console"); + + hud.jsterm.clearOutput(); + hud.jsterm.execute(input); + + return waitForMessages({ + webconsole: hud, + messages: [message] + }); + } + + function* checkWidgetAndMessage(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, msg}; + } + + function* checkNodeHighlight(toolbox, inspector, msg, testData) { + 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"); + let onNodeHighlight = toolbox.once("node-highlight"); + + EventUtils.synthesizeMouseAtCenter(inspectorIcon, {}, + inspectorIcon.ownerDocument.defaultView); + yield onInspectorSelected; + yield onInspectorUpdated; + yield onNodeHighlight; + let nodeFront = yield onNewNode; + + ok(true, "Inspector selected and new node got selected"); + + is(nodeFront.displayName, testData.displayName, + "The correct node was highlighted"); + + if (testData.attrs) { + let attrs = nodeFront.attributes; + for (let i in testData.attrs) { + is(attrs[i].name, testData.attrs[i].name, + "Expected attribute's name is present"); + is(attrs[i].value, testData.attrs[i].value, + "Expected attribute's value is present"); + } + } + + info("Unhighlight the node by moving away from the markup view"); + let onNodeUnhighlight = toolbox.once("node-unhighlight"); + let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button"); + EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, + inspector.toolbox.win); + yield onNodeUnhighlight; + + info("Switching back to the console"); + yield toolbox.selectTool("webconsole"); + } + + return Task.spawn(runner); +} + +/** + * Finish the request and resolve with the request object. + * + * @param {Function} predicate A predicate function that takes the request + * object as an argument and returns true if the request was the expected one, + * false otherwise. The returned promise is resolved ONLY if the predicate + * matches a request. Defaults to accepting any request. + * @return promise + * @resolves The request object. + */ +function waitForFinishedRequest(predicate = () => true) { + registerCleanupFunction(function () { + HUDService.lastFinishedRequest.callback = null; + }); + + return new Promise(resolve => { + HUDService.lastFinishedRequest.callback = request => { + // Check if this is the expected request + if (predicate(request)) { + // Match found. Clear the listener. + HUDService.lastFinishedRequest.callback = null; + + resolve(request); + } else { + info(`Ignoring unexpected request ${JSON.stringify(request, null, 2)}`); + } + }; + }); +} + +/** + * 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; +} + +/** + * Checks a link to the inspector + * + * @param {boolean} hasLinkToInspector Set to true if the message should + * link to the inspector panel. + * @param {element} msg The message to test. + */ +function checkLinkToInspector(hasLinkToInspector, msg) { + let elementNodeWidget = [...msg._messageObject.widgets][0]; + if (!elementNodeWidget) { + ok(!hasLinkToInspector, "The message has no ElementNode widget"); + return true; + } + + return elementNodeWidget.linkToInspector().then(() => { + // linkToInspector resolved, check for the .open-inspector element + if (hasLinkToInspector) { + 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(!hasLinkToInspector, + "The ElementNode widget isn't linked to the inspector"); + }); +} + +function getSourceActor(sources, URL) { + let item = sources.getItemForAttachment(a => a.source.url === URL); + return item && item.value; +} + +/** + * Make a request against an actor and resolve with the packet. + * @param object client + * The client to use when making the request. + * @param function requestType + * The client request function to run. + * @param array args + * The arguments to pass into the function. + */ +function getPacket(client, requestType, args) { + return new Promise(resolve => { + client[requestType](...args, packet => resolve(packet)); + }); +} + +/** + * 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; +} + +function getRenderedSource(root) { + let location = root.querySelector(".message-location .frame-link"); + return location ? { + url: location.getAttribute("data-url"), + line: location.getAttribute("data-line"), + column: location.getAttribute("data-column"), + } : null; +} diff --git a/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html b/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html new file mode 100644 index 0000000000..ba5212de33 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-585956-console-trace.html b/devtools/client/webconsole/test/test-bug-585956-console-trace.html new file mode 100644 index 0000000000..e658ba633d --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html new file mode 100644 index 0000000000..ebf9c515fe --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html new file mode 100644 index 0000000000..8e47cf20f9 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"></iframe> + </body> + </html> diff --git a/devtools/client/webconsole/test/test-bug-595934-canvas-css.html b/devtools/client/webconsole/test/test-bug-595934-canvas-css.html new file mode 100644 index 0000000000..3c9cf03a52 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-canvas-css.js b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js new file mode 100644 index 0000000000..ee1ebd4251 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js @@ -0,0 +1,10 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +window.addEventListener("DOMContentLoaded", function () { + var canvas = document.querySelector("canvas"); + var context = canvas.getContext("2d"); + context.strokeStyle = "foobarCanvasCssParser"; +}, false); diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.css b/devtools/client/webconsole/test/test-bug-595934-css-loader.css new file mode 100644 index 0000000000..b4224430f6 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^ new file mode 100644 index 0000000000..e7be84a714 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^ @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.html b/devtools/client/webconsole/test/test-bug-595934-css-loader.html new file mode 100644 index 0000000000..6bb0d54c5e --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-css-parser.css b/devtools/client/webconsole/test/test-bug-595934-css-parser.css new file mode 100644 index 0000000000..f6db823987 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-css-parser.html b/devtools/client/webconsole/test/test-bug-595934-css-parser.html new file mode 100644 index 0000000000..a4ea74ba38 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html new file mode 100644 index 0000000000..a70f9011b3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js new file mode 100644 index 0000000000..bf9ccece9e --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js @@ -0,0 +1,8 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +window.addEventListener("load", function () { + document.getElementById(""); +}, false); diff --git a/devtools/client/webconsole/test/test-bug-595934-html.html b/devtools/client/webconsole/test/test-bug-595934-html.html new file mode 100644 index 0000000000..fe35afef66 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-image.html b/devtools/client/webconsole/test/test-bug-595934-image.html new file mode 100644 index 0000000000..312ecd49fb --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-image.jpg b/devtools/client/webconsole/test/test-bug-595934-image.jpg Binary files differnew file mode 100644 index 0000000000..947e5f11ba --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-595934-image.jpg diff --git a/devtools/client/webconsole/test/test-bug-595934-imagemap.html b/devtools/client/webconsole/test/test-bug-595934-imagemap.html new file mode 100644 index 0000000000..007c3c01bd --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html new file mode 100644 index 0000000000..2fd8beac58 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml new file mode 100644 index 0000000000..4812786f10 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml b/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml new file mode 100644 index 0000000000..62689c567c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-svg.xhtml b/devtools/client/webconsole/test/test-bug-595934-svg.xhtml new file mode 100644 index 0000000000..572382c64d --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-workers.html b/devtools/client/webconsole/test/test-bug-595934-workers.html new file mode 100644 index 0000000000..baf5a62157 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-595934-workers.js b/devtools/client/webconsole/test/test-bug-595934-workers.js new file mode 100644 index 0000000000..d23f080af2 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-595934-workers.js @@ -0,0 +1,14 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +/* global fooBarWorker*/ +/* eslint-disable no-unused-vars*/ + +"use strict"; + +var onmessage = function () { + fooBarWorker(); +}; + diff --git a/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html new file mode 100644 index 0000000000..25bdeecc52 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js new file mode 100644 index 0000000000..00821e38e2 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js @@ -0,0 +1,9 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +function f() { + bogus.g(); +} + diff --git a/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html b/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html new file mode 100644 index 0000000000..68e19e6775 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs b/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs new file mode 100644 index 0000000000..2e78d6b7b7 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-600183-charset.html b/devtools/client/webconsole/test/test-bug-600183-charset.html new file mode 100644 index 0000000000..040490a6b0 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^ new file mode 100644 index 0000000000..9f3e2302f5 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=gb2312 diff --git a/devtools/client/webconsole/test/test-bug-601177-log-levels.html b/devtools/client/webconsole/test/test-bug-601177-log-levels.html new file mode 100644 index 0000000000..a592139077 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-601177-log-levels.js b/devtools/client/webconsole/test/test-bug-601177-log-levels.js new file mode 100644 index 0000000000..afeb13ff62 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-601177-log-levels.js @@ -0,0 +1,8 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +foobarBug601177strictError = "strict error"; + +window.foobarBug601177exception(); diff --git a/devtools/client/webconsole/test/test-bug-603750-websocket.html b/devtools/client/webconsole/test/test-bug-603750-websocket.html new file mode 100644 index 0000000000..f0097dd770 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-603750-websocket.js b/devtools/client/webconsole/test/test-bug-603750-websocket.js new file mode 100644 index 0000000000..3d92c506b1 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-603750-websocket.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +window.addEventListener("load", function () { + let ws1 = new WebSocket("ws://0.0.0.0:81"); + ws1.onopen = function () { + ws1.send("test 1"); + ws1.close(); + }; + + let 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/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html new file mode 100644 index 0000000000..451eba21e6 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html new file mode 100644 index 0000000000..fdb636b974 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html b/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html new file mode 100644 index 0000000000..edf40e80ed --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html b/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html new file mode 100644 index 0000000000..ac755e1b97 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html b/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html new file mode 100644 index 0000000000..09c9867038 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs b/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs new file mode 100644 index 0000000000..f92e0fe658 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-632275-getters.html b/devtools/client/webconsole/test/test-bug-632275-getters.html new file mode 100644 index 0000000000..349c301f3c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html b/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html new file mode 100644 index 0000000000..1eddcf3501 --- /dev/null +++ b/devtools/client/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 = (function* () { for (let i in _container.iter2) yield i * 2; })(); +})(); +</script> + </head> + <body> + <p>Web Console test for bug 632347 - iterators and generators.</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-644419-log-limits.html b/devtools/client/webconsole/test/test-bug-644419-log-limits.html new file mode 100644 index 0000000000..21d99ba148 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-646025-console-file-location.html b/devtools/client/webconsole/test/test-bug-646025-console-file-location.html new file mode 100644 index 0000000000..7c80f14461 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-658368-time-methods.html b/devtools/client/webconsole/test/test-bug-658368-time-methods.html new file mode 100644 index 0000000000..cc50b63134 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html b/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html new file mode 100644 index 0000000000..db83274f09 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html new file mode 100644 index 0000000000..ccb363ed9e --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html new file mode 100644 index 0000000000..b9939fe83c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html new file mode 100644 index 0000000000..7678d15fe0 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html new file mode 100644 index 0000000000..233a6cb70a --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html new file mode 100644 index 0000000000..da0d588193 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html new file mode 100644 index 0000000000..f33f0a6dcf --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html new file mode 100644 index 0000000000..c0ff6994ad --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html new file mode 100644 index 0000000000..84e0b6c728 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html" sandbox="allow-scripts allow-same-origin"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html new file mode 100644 index 0000000000..72d86931ad --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html new file mode 100644 index 0000000000..d7bcd45d65 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html new file mode 100644 index 0000000000..f473303f44 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html"> + </iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-766001-console-log.js b/devtools/client/webconsole/test/test-bug-766001-console-log.js new file mode 100644 index 0000000000..a4be0cb155 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-766001-console-log.js @@ -0,0 +1,10 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +function onLoad123() { + console.log("Blah Blah"); +} + +window.addEventListener("load", onLoad123, false); diff --git a/devtools/client/webconsole/test/test-bug-766001-js-console-links.html b/devtools/client/webconsole/test/test-bug-766001-js-console-links.html new file mode 100644 index 0000000000..6a6ac60089 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-766001-js-errors.js b/devtools/client/webconsole/test/test-bug-766001-js-errors.js new file mode 100644 index 0000000000..85321813a1 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug-766001-js-errors.js @@ -0,0 +1,8 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +window.addEventListener("load", function () { + document.bar(); +}, false); diff --git a/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css new file mode 100644 index 0000000000..ad7fd19995 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css new file mode 100644 index 0000000000..91b14137a2 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-782653-css-errors.html b/devtools/client/webconsole/test/test-bug-782653-css-errors.html new file mode 100644 index 0000000000..7ca11fc340 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-837351-security-errors.html b/devtools/client/webconsole/test/test-bug-837351-security-errors.html new file mode 100644 index 0000000000..db83274f09 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html b/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html new file mode 100644 index 0000000000..51bc0de289 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-869003-iframe.html b/devtools/client/webconsole/test/test-bug-869003-iframe.html new file mode 100644 index 0000000000..5a29728e57 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-869003-top-window.html b/devtools/client/webconsole/test/test-bug-869003-top-window.html new file mode 100644 index 0000000000..a2da438f6d --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-869003-iframe.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html b/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html new file mode 100644 index 0000000000..de297d9b57 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html b/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html new file mode 100644 index 0000000000..54a4e90383 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html new file mode 100644 index 0000000000..912e301f02 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf-8"> + <title>Click on function should point to source</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript" src="test-bug_1050691_click_function_to_source.js"></script> + </head> + <body></body> +</html> diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js new file mode 100644 index 0000000000..1eddf0d6ed --- /dev/null +++ b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js @@ -0,0 +1,10 @@ +/** + * this + * is + * a + * function + */ +function foo() { + console.log(foo); +} + diff --git a/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html b/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html new file mode 100644 index 0000000000..f2d650a5d9 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-bug_923281_test1.js b/devtools/client/webconsole/test/test-bug_923281_test1.js new file mode 100644 index 0000000000..1c07f11559 --- /dev/null +++ b/devtools/client/webconsole/test/test-bug_923281_test1.js @@ -0,0 +1,7 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/ */ + +console.log("Sample log."); +console.log("This log should be filtered when filtered for test2.js."); diff --git a/devtools/client/webconsole/test/test-bug_923281_test2.js b/devtools/client/webconsole/test/test-bug_923281_test2.js new file mode 100644 index 0000000000..7ac85b387a --- /dev/null +++ b/devtools/client/webconsole/test/test-bug_923281_test2.js @@ -0,0 +1,8 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +console.log("This is a random text."); diff --git a/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html b/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html new file mode 100644 index 0000000000..ab44de09f1 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-certificate-messages.html b/devtools/client/webconsole/test/test-certificate-messages.html new file mode 100644 index 0000000000..b0419a6fc3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-closure-optimized-out.html b/devtools/client/webconsole/test/test-closure-optimized-out.html new file mode 100644 index 0000000000..3ad4e8fc0a --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-closures.html b/devtools/client/webconsole/test/test-closures.html new file mode 100644 index 0000000000..4fadade207 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-api-stackframe.html b/devtools/client/webconsole/test/test-console-api-stackframe.html new file mode 100644 index 0000000000..df7fef9b11 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-assert.html b/devtools/client/webconsole/test/test-console-assert.html new file mode 100644 index 0000000000..b104d72d43 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-clear.html b/devtools/client/webconsole/test/test-console-clear.html new file mode 100644 index 0000000000..8009db858d --- /dev/null +++ b/devtools/client/webconsole/test/test-console-clear.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console.clear() tests</title> + <script type="text/javascript"> + console.log("log1"); + console.log("log2"); + console.clear(); + + window.objFromPage = { a: 1 }; + </script> + </head> + <body> + <h1 id="header">Clear Demo</h1> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-console-column.html b/devtools/client/webconsole/test/test-console-column.html new file mode 100644 index 0000000000..ff9cc81e10 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-count-external-file.js b/devtools/client/webconsole/test/test-console-count-external-file.js new file mode 100644 index 0000000000..cca9e2f10a --- /dev/null +++ b/devtools/client/webconsole/test/test-console-count-external-file.js @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ + +"use strict"; + +function counterExternalFile() { + console.count("console.count() testcounter"); +} +function externalCountersWithoutLabel() { + console.count(); + console.count(); +} diff --git a/devtools/client/webconsole/test/test-console-count.html b/devtools/client/webconsole/test/test-console-count.html new file mode 100644 index 0000000000..e6db0ebb01 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-extras.html b/devtools/client/webconsole/test/test-console-extras.html new file mode 100644 index 0000000000..8685b1a800 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-extras.html @@ -0,0 +1,18 @@ +<!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.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/devtools/client/webconsole/test/test-console-output-02.html b/devtools/client/webconsole/test/test-console-output-02.html new file mode 100644 index 0000000000..ad90f0ebfe --- /dev/null +++ b/devtools/client/webconsole/test/test-console-output-02.html @@ -0,0 +1,66 @@ +<!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 bunnies = new String("bunnies") +var weakset = new WeakSet([bunnies, document.head]); + +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); + +var weakmap = new WeakMap([[bunnies, 23], [document.body.children, testobj2]]); + + </script> +</body> +</html> diff --git a/devtools/client/webconsole/test/test-console-output-03.html b/devtools/client/webconsole/test/test-console-output-03.html new file mode 100644 index 0000000000..9dcf051a63 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-output-04.html b/devtools/client/webconsole/test/test-console-output-04.html new file mode 100644 index 0000000000..bb43452772 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-output-dom-elements.html b/devtools/client/webconsole/test/test-console-output-dom-elements.html new file mode 100644 index 0000000000..5acabfa3fb --- /dev/null +++ b/devtools/client/webconsole/test/test-console-output-dom-elements.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - dom elements</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> + <p id="lots-of-attributes" a b c d e f g h i j k l m n></p> + <!-- + Be sure we have a charset in our iframe's data URI, otherwise we get the following extra + console output message: + "The character encoding of a framed document was not declared. The document may appear different if viewed without the document framing it." + This wouldn't be a big deal, but when we look for a "<p>" in our `waitForMessage` helper, + this extra encoding warning line contains the data URI source, returning a message + that was unexpected + --> + <iframe src="data:text/html;charset=US-ASCII,<p>hello from iframe</p>"></iframe> + <div class="some classname here with more classnames here"></div> + <svg> + <clipPath> + <rect x="0" y="0" width="10" height="5"></rect> + </clipPath> + </svg> + <script type="text/javascript"> +function testBodyNode() { + return document.body; +} + +function testDocumentElement() { + return document.documentElement; +} + +function testLotsOfAttributes() { + return document.querySelector("#lots-of-attributes"); +} + +function testDocument() { + return document; +} + +function testNode() { + return document.querySelector("p"); +} + +function testSvgNode() { + return document.querySelector("clipPath"); +} + +function testNodeList() { + return document.querySelectorAll("body *"); +} + +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/devtools/client/webconsole/test/test-console-output-events.html b/devtools/client/webconsole/test/test-console-output-events.html new file mode 100644 index 0000000000..908a86fabd --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-output-regexp.html b/devtools/client/webconsole/test/test-console-output-regexp.html new file mode 100644 index 0000000000..e62680d904 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-output-regexp.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output for RegExp</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"> +Object.defineProperty(RegExp.prototype, "flags", { + get: function() { throw Error("flags called"); } +}) +Object.defineProperty(RegExp.prototype, "source", { + get: function() { throw Error("source called"); }, +}) + </script> +</body> +</html> diff --git a/devtools/client/webconsole/test/test-console-replaced-api.html b/devtools/client/webconsole/test/test-console-replaced-api.html new file mode 100644 index 0000000000..2b05d023ac --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-console-server-logging-array.sjs b/devtools/client/webconsole/test/test-console-server-logging-array.sjs new file mode 100644 index 0000000000..bba3942642 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-server-logging-array.sjs @@ -0,0 +1,32 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) +{ + var page = "<!DOCTYPE html><html>" + + "<head><meta charset='utf-8'></head>" + + "<body><p>hello world!</p></body>" + + "</html>"; + + var data = { + "version": "4.1.0", + "columns": ["log", "backtrace", "type"], + "rows":[[ + [{ "best": "Firefox", "reckless": "Chrome", "new_ie": "Safari", "new_new_ie": "Edge"}], + "C:\\src\\www\\serverlogging\\test7.php:4:1", + "" + ]], + }; + + // Put log into headers. + var value = b64EncodeUnicode(JSON.stringify(data)); + response.setHeader("X-ChromeLogger-Data", value, false); + + response.write(page); +} + +function b64EncodeUnicode(str) { + return btoa(unescape(encodeURIComponent(str))); +} diff --git a/devtools/client/webconsole/test/test-console-server-logging.sjs b/devtools/client/webconsole/test/test-console-server-logging.sjs new file mode 100644 index 0000000000..7177e71852 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-server-logging.sjs @@ -0,0 +1,32 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) +{ + var page = "<!DOCTYPE html><html>" + + "<head><meta charset='utf-8'></head>" + + "<body><p>hello world!</p></body>" + + "</html>"; + + var data = { + "version": "4.1.0", + "columns": ["log", "backtrace", "type"], + "rows": [[ + ["values: %s %o %i %f %s","string",{"a":10,"___class_name":"Object"},123,1.12, "\u2713"], + "C:\\src\\www\\serverlogging\\test7.php:4:1", + "" + ]] + }; + + // Put log into headers. + var value = b64EncodeUnicode(JSON.stringify(data)); + response.setHeader("X-ChromeLogger-Data", value, false); + + response.write(page); +} + +function b64EncodeUnicode(str) { + return btoa(unescape(encodeURIComponent(str))); +} diff --git a/devtools/client/webconsole/test/test-console-table.html b/devtools/client/webconsole/test/test-console-table.html new file mode 100644 index 0000000000..461dedcde8 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-table.html @@ -0,0 +1,63 @@ +<!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); + + // These are globals and so won't be reclaimed by the GC. + var bunnies = new String("bunnies"); + var lizards = new String("lizards"); + + var weakmap = new WeakMap(); + weakmap.set(bunnies, 23); + weakmap.set(lizards, "oh no"); + + var weakset = new WeakSet([bunnies, lizards]); + + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-console-trace-async.html b/devtools/client/webconsole/test/test-console-trace-async.html new file mode 100644 index 0000000000..c7b895455e --- /dev/null +++ b/devtools/client/webconsole/test/test-console-trace-async.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + <head><meta charset="utf-8"> + <title>Web Console test for bug 1200832 - console.trace() async stacks</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<script type="application/javascript"> +function inner() { + console.trace(); +} + +function time1() { + new Promise(function(resolve, reject) { + setTimeout(resolve, 10); + }).then(inner); +} + +setTimeout(time1, 10); +</script> + </head> + <body> + <p>Web Console test for bug 1200832 - console.trace() async stacks</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-console-workers.html b/devtools/client/webconsole/test/test-console-workers.html new file mode 100644 index 0000000000..f4b286ae57 --- /dev/null +++ b/devtools/client/webconsole/test/test-console-workers.html @@ -0,0 +1,13 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"><head> + <meta charset="utf-8"> + <title>Console test</title> + </head> + <body> + <script type="text/javascript"> +var sw = new SharedWorker('data:application/javascript,console.log("foo-bar-shared-worker");'); + </script> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-console.html b/devtools/client/webconsole/test/test-console.html new file mode 100644 index 0000000000..b294a3ba1c --- /dev/null +++ b/devtools/client/webconsole/test/test-console.html @@ -0,0 +1,34 @@ +<!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"> + var fooObj = { + testProp: "testValue" + }; + + function test() { + var str = "Dolske Digs Bacon, Now and Forevermore." + for (var i=0; i < 5; i++) { + console.log(str); + } + } + + function testTrace() { + console.log("bug 1100562"); + console.trace(); + } + + 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> + <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button> + <div id="myDiv"></div> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-consoleiframes.html b/devtools/client/webconsole/test/test-consoleiframes.html new file mode 100644 index 0000000000..a8176f93ad --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-cu-reporterror.js b/devtools/client/webconsole/test/test-cu-reporterror.js new file mode 100644 index 0000000000..6e2f9d2623 --- /dev/null +++ b/devtools/client/webconsole/test/test-cu-reporterror.js @@ -0,0 +1,4 @@ +function a() { + Components.utils.reportError("bug1141222"); +} +a(); diff --git a/devtools/client/webconsole/test/test-data.json b/devtools/client/webconsole/test/test-data.json new file mode 100644 index 0000000000..471d240b5d --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-data.json^headers^ b/devtools/client/webconsole/test/test-data.json^headers^ new file mode 100644 index 0000000000..7b5e82d4b7 --- /dev/null +++ b/devtools/client/webconsole/test/test-data.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json diff --git a/devtools/client/webconsole/test/test-duplicate-error.html b/devtools/client/webconsole/test/test-duplicate-error.html new file mode 100644 index 0000000000..1b2691672c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html b/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html new file mode 100644 index 0000000000..cf19629f47 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-error.html b/devtools/client/webconsole/test/test-error.html new file mode 100644 index 0000000000..abf62a3f10 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-eval-in-stackframe.html b/devtools/client/webconsole/test/test-eval-in-stackframe.html new file mode 100644 index 0000000000..ec1bf3f306 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-exception-stackframe.html b/devtools/client/webconsole/test/test-exception-stackframe.html new file mode 100644 index 0000000000..0a6dea4cad --- /dev/null +++ b/devtools/client/webconsole/test/test-exception-stackframe.html @@ -0,0 +1,43 @@ +<!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 1184172 - stacktraces for exceptions</title> + <script> + function firstCall() { + secondCall(); + } + + // Check anonymous functions + var secondCall = function () { + thirdCall(); + } + + function thirdCall() { + nonExistingMethodCall(); + } + + function domAPI() { + document.querySelector("buggy;selector"); + } + + function domException() { + throw new DOMException("DOMException"); + } + window.addEventListener("load", firstCall); + window.addEventListener("load", function onLoadDomAPI() { + domAPI(); + }); + window.addEventListener("load", function onLoadDomException() { + domException(); + }); + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-file-location.js b/devtools/client/webconsole/test/test-file-location.js new file mode 100644 index 0000000000..d9879a3565 --- /dev/null +++ b/devtools/client/webconsole/test/test-file-location.js @@ -0,0 +1,12 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +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/devtools/client/webconsole/test/test-filter.html b/devtools/client/webconsole/test/test-filter.html new file mode 100644 index 0000000000..219177bb29 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-for-of.html b/devtools/client/webconsole/test/test-for-of.html new file mode 100644 index 0000000000..876010c9ef --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html new file mode 100644 index 0000000000..d14b5cdd7c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html new file mode 100644 index 0000000000..dde47a78e3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html"> + </iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-iframe1.html b/devtools/client/webconsole/test/test-iframe1.html new file mode 100644 index 0000000000..4dd4eddfed --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe2.html b/devtools/client/webconsole/test/test-iframe2.html new file mode 100644 index 0000000000..c15884795f --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-iframe3.html b/devtools/client/webconsole/test/test-iframe3.html new file mode 100644 index 0000000000..f0df8b6692 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-image.png b/devtools/client/webconsole/test/test-image.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/devtools/client/webconsole/test/test-image.png diff --git a/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html b/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html new file mode 100644 index 0000000000..cb8cfdaaf5 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-mutation.html b/devtools/client/webconsole/test/test-mutation.html new file mode 100644 index 0000000000..e80933b06d --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-network-request.html b/devtools/client/webconsole/test/test-network-request.html new file mode 100644 index 0000000000..7cb736296d --- /dev/null +++ b/devtools/client/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/devtools/client/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/devtools/client/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/devtools/client/webconsole/test/test-network.html b/devtools/client/webconsole/test/test-network.html new file mode 100644 index 0000000000..69d3422e32 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-observe-http-ajax.html b/devtools/client/webconsole/test/test-observe-http-ajax.html new file mode 100644 index 0000000000..5abcefdad0 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-own-console.html b/devtools/client/webconsole/test/test-own-console.html new file mode 100644 index 0000000000..d1d18ebc2c --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-property-provider.html b/devtools/client/webconsole/test/test-property-provider.html new file mode 100644 index 0000000000..532b00f448 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-repeated-messages.html b/devtools/client/webconsole/test/test-repeated-messages.html new file mode 100644 index 0000000000..b19c9485e0 --- /dev/null +++ b/devtools/client/webconsole/test/test-repeated-messages.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <title>Test for bugs 720180, 800510, 865288 and 1218089</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); + } + } + function testConsoleFalsyValues(){ + [NaN, undefined, null].forEach(function(item, index){ + console.log(item); + }); + [NaN, NaN].forEach(function(item, index){ + console.log(item); + }); + [undefined, undefined].forEach(function(item, index){ + console.log(item); + }); + [null, null].forEach(function(item, index){ + console.log(item); + }); + } + </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/devtools/client/webconsole/test/test-result-format-as-string.html b/devtools/client/webconsole/test/test-result-format-as-string.html new file mode 100644 index 0000000000..c3ab78ee7f --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html new file mode 100644 index 0000000000..17f0e459e3 --- /dev/null +++ b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <iframe src="http://tracking.example.com/"></iframe> + </body> +</html> diff --git a/devtools/client/webconsole/test/test-webconsole-error-observer.html b/devtools/client/webconsole/test/test-webconsole-error-observer.html new file mode 100644 index 0000000000..8466bc6f2f --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html new file mode 100644 index 0000000000..bf63601bf3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ new file mode 100644 index 0000000000..0b234f0e89 --- /dev/null +++ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: reflected-xss filter; diff --git a/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html new file mode 100644 index 0000000000..ebb7773cb8 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js new file mode 100644 index 0000000000..c7d5cec144 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^ new file mode 100644 index 0000000000..f99377fc62 --- /dev/null +++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^ @@ -0,0 +1 @@ +Strict-Transport-Security: some complete nonsense diff --git a/devtools/client/webconsole/test/test_bug_1010953_cspro.html b/devtools/client/webconsole/test/test_bug_1010953_cspro.html new file mode 100644 index 0000000000..83ac6391f1 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^ b/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^ new file mode 100644 index 0000000000..03056e2cb3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug_1247459_violation.html b/devtools/client/webconsole/test/test_bug_1247459_violation.html new file mode 100644 index 0000000000..fdda4eb262 --- /dev/null +++ b/devtools/client/webconsole/test/test_bug_1247459_violation.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta> + <meta http-equiv="Content-Security-Policy" content="img-src https://example.com"></meta> + <meta charset="UTF-8"> + <title>Test for Bug 1247459 - policy violations for header and META are displayed separately</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=1247459">Mozilla Bug 1247459</a> +<img src="http://some.example.com/test.png"> +</body> +</html> diff --git a/devtools/client/webconsole/test/test_bug_770099_violation.html b/devtools/client/webconsole/test/test_bug_770099_violation.html new file mode 100644 index 0000000000..ccbded87a3 --- /dev/null +++ b/devtools/client/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/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^ new file mode 100644 index 0000000000..4c6fa3c26a --- /dev/null +++ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs new file mode 100644 index 0000000000..cd0e18523b --- /dev/null +++ b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + let issue; + switch (request.queryString) { + case "badSyntax": + response.setHeader("Public-Key-Pins", "\""); + issue = "is not syntactically correct."; + break; + case "noMaxAge": + response.setHeader("Public-Key-Pins", "max-age444"); + issue = "does not include a max-age directive."; + break; + case "invalidIncludeSubDomains": + response.setHeader("Public-Key-Pins", "includeSubDomains=abc"); + issue = "includes an invalid includeSubDomains directive."; + break; + case "invalidMaxAge": + response.setHeader("Public-Key-Pins", "max-age=abc"); + issue = "includes an invalid max-age directive."; + break; + case "multipleIncludeSubDomains": + response.setHeader("Public-Key-Pins", + "includeSubDomains; includeSubDomains"); + issue = "includes multiple includeSubDomains directives."; + break; + case "multipleMaxAge": + response.setHeader("Public-Key-Pins", + "max-age=444; max-age=999"); + issue = "includes multiple max-age directives."; + break; + case "multipleReportURIs": + response.setHeader("Public-Key-Pins", + 'report-uri="http://example.com"; ' + + 'report-uri="http://example.com"'); + issue = "includes multiple report-uri directives."; + break; + case "pinsetDoesNotMatch": + response.setHeader( + "Public-Key-Pins", + 'max-age=999; ' + + 'pin-sha256="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; ' + + 'pin-sha256="BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="'); + issue = "does not include a matching pin."; + break; + } + + response.write("This page is served with a PKP header that " + issue); +} diff --git a/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs new file mode 100644 index 0000000000..9e3ea76240 --- /dev/null +++ b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + let issue; + switch (request.queryString) { + case "badSyntax": + response.setHeader("Strict-Transport-Security", "\""); + issue = "is not syntactically correct."; + break; + case "noMaxAge": + response.setHeader("Strict-Transport-Security", "max-age444"); + issue = "does not include a max-age directive."; + break; + case "invalidIncludeSubDomains": + response.setHeader("Strict-Transport-Security", "includeSubDomains=abc"); + issue = "includes an invalid includeSubDomains directive."; + break; + case "invalidMaxAge": + response.setHeader("Strict-Transport-Security", "max-age=abc"); + issue = "includes an invalid max-age directive."; + break; + case "multipleIncludeSubDomains": + response.setHeader("Strict-Transport-Security", + "includeSubDomains; includeSubDomains"); + issue = "includes multiple includeSubDomains directives."; + break; + case "multipleMaxAge": + response.setHeader("Strict-Transport-Security", + "max-age=444; max-age=999"); + issue = "includes multiple max-age directives."; + break; + } + + response.write("This page is served with a STS header that " + issue); +} diff --git a/devtools/client/webconsole/test/testscript.js b/devtools/client/webconsole/test/testscript.js new file mode 100644 index 0000000000..849b03d86e --- /dev/null +++ b/devtools/client/webconsole/test/testscript.js @@ -0,0 +1,2 @@ +"use strict"; +console.log("running network console logging tests"); diff --git a/devtools/client/webconsole/utils.js b/devtools/client/webconsole/utils.js new file mode 100644 index 0000000000..ae2f1809f1 --- /dev/null +++ b/devtools/client/webconsole/utils.js @@ -0,0 +1,395 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const {Cc, Ci, Cu, components} = require("chrome"); +const Services = require("Services"); +const {LocalizationHelper} = require("devtools/shared/l10n"); + +// Match the function name from the result of toString() or toSource(). +// +// Examples: +// (function foobar(a, b) { ... +// function foobar2(a) { ... +// function() { ... +const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; + +// Number of terminal entries for the self-xss prevention to go away +const CONSOLE_ENTRY_THRESHOLD = 5; + +const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [ + "SharedWorker", + "ServiceWorker", + "Worker" +]; + +var WebConsoleUtils = { + + /** + * Wrap a string in an nsISupportsString object. + * + * @param string string + * @return nsISupportsString + */ + supportsString: function (string) { + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = string; + return str; + }, + + /** + * Clone an object. + * + * @param object object + * The object you want cloned. + * @param boolean recursive + * Tells if you want to dig deeper into the object, to clone + * recursively. + * @param function [filter] + * Optional, filter function, called for every property. Three + * arguments are passed: key, value and object. Return true if the + * property should be added to the cloned object. Return false to skip + * the property. + * @return object + * The cloned object. + */ + cloneObject: function (object, recursive, filter) { + if (typeof object != "object") { + return object; + } + + let temp; + + if (Array.isArray(object)) { + temp = []; + Array.forEach(object, function (value, index) { + if (!filter || filter(index, value, object)) { + temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value); + } + }); + } else { + temp = {}; + for (let key in object) { + let value = object[key]; + if (object.hasOwnProperty(key) && + (!filter || filter(key, value, object))) { + temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value; + } + } + } + + return temp; + }, + + /** + * Copies certain style attributes from one element to another. + * + * @param nsIDOMNode from + * The target node. + * @param nsIDOMNode to + * The destination node. + */ + copyTextStyles: function (from, to) { + let win = from.ownerDocument.defaultView; + let style = win.getComputedStyle(from); + to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText; + to.style.fontSize = style.getPropertyCSSValue("font-size").cssText; + to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText; + to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText; + }, + + /** + * Create a grip for the given value. If the value is an object, + * an object wrapper will be created. + * + * @param mixed value + * The value you want to create a grip for, before sending it to the + * client. + * @param function objectWrapper + * If the value is an object then the objectWrapper function is + * invoked to give us an object grip. See this.getObjectGrip(). + * @return mixed + * The value grip. + */ + createValueGrip: function (value, objectWrapper) { + switch (typeof value) { + case "boolean": + return value; + case "string": + return objectWrapper(value); + case "number": + if (value === Infinity) { + return { type: "Infinity" }; + } else if (value === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(value)) { + return { type: "NaN" }; + } else if (!value && 1 / value === -Infinity) { + return { type: "-0" }; + } + return value; + case "undefined": + return { type: "undefined" }; + case "object": + if (value === null) { + return { type: "null" }; + } + // Fall through. + case "function": + return objectWrapper(value); + default: + console.error("Failed to provide a grip for value of " + typeof value + + ": " + value); + return null; + } + }, + + /** + * Determine if the given request mixes HTTP with HTTPS content. + * + * @param string request + * Location of the requested content. + * @param string location + * Location of the current page. + * @return boolean + * True if the content is mixed, false if not. + */ + isMixedHTTPSRequest: function (request, location) { + try { + let requestURI = Services.io.newURI(request, null, null); + let contentURI = Services.io.newURI(location, null, null); + return (contentURI.scheme == "https" && requestURI.scheme != "https"); + } catch (ex) { + return false; + } + }, + + /** + * Helper function to deduce the name of the provided function. + * + * @param funtion function + * The function whose name will be returned. + * @return string + * Function name. + */ + getFunctionName: function (func) { + let name = null; + if (func.name) { + name = func.name; + } else { + let desc; + try { + desc = func.getOwnPropertyDescriptor("displayName"); + } catch (ex) { + // Ignore. + } + if (desc && typeof desc.value == "string") { + name = desc.value; + } + } + if (!name) { + try { + let str = (func.toString() || func.toSource()) + ""; + name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1]; + } catch (ex) { + // Ignore. + } + } + return name; + }, + + /** + * Get the object class name. For example, the |window| object has the Window + * class name (based on [object Window]). + * + * @param object object + * The object you want to get the class name for. + * @return string + * The object class name. + */ + getObjectClassName: function (object) { + if (object === null) { + return "null"; + } + if (object === undefined) { + return "undefined"; + } + + let type = typeof object; + if (type != "object") { + // Grip class names should start with an uppercase letter. + return type.charAt(0).toUpperCase() + type.substr(1); + } + + let className; + + try { + className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1]; + if (!className) { + className = ((object.constructor + "") + .match(/^\[object (\S+)\]$/) || [])[1]; + } + if (!className && typeof object.constructor == "function") { + className = this.getFunctionName(object.constructor); + } + } catch (ex) { + // Ignore. + } + + return className; + }, + + /** + * Check if the given value is a grip with an actor. + * + * @param mixed grip + * Value you want to check if it is a grip with an actor. + * @return boolean + * True if the given value is a grip with an actor. + */ + isActorGrip: function (grip) { + return grip && typeof (grip) == "object" && grip.actor; + }, + + /** + * Value of devtools.selfxss.count preference + * + * @type number + * @private + */ + _usageCount: 0, + get usageCount() { + if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) { + WebConsoleUtils._usageCount = + Services.prefs.getIntPref("devtools.selfxss.count"); + if (Services.prefs.getBoolPref("devtools.chrome.enabled")) { + WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD; + } + } + return WebConsoleUtils._usageCount; + }, + set usageCount(newUC) { + if (newUC <= CONSOLE_ENTRY_THRESHOLD) { + WebConsoleUtils._usageCount = newUC; + Services.prefs.setIntPref("devtools.selfxss.count", newUC); + } + }, + /** + * The inputNode "paste" event handler generator. Helps prevent + * self-xss attacks + * + * @param nsIDOMElement inputField + * @param nsIDOMElement notificationBox + * @returns A function to be added as a handler to 'paste' and + *'drop' events on the input field + */ + pasteHandlerGen: function (inputField, notificationBox, msg, okstring) { + let handler = function (event) { + if (WebConsoleUtils.usageCount >= CONSOLE_ENTRY_THRESHOLD) { + inputField.removeEventListener("paste", handler); + inputField.removeEventListener("drop", handler); + return true; + } + if (notificationBox.getNotificationWithValue("selfxss-notification")) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + let notification = notificationBox.appendNotification(msg, + "selfxss-notification", null, + notificationBox.PRIORITY_WARNING_HIGH, null, + function (eventType) { + // Cleanup function if notification is dismissed + if (eventType == "removed") { + inputField.removeEventListener("keyup", pasteKeyUpHandler); + } + }); + + function pasteKeyUpHandler(event2) { + let value = inputField.value || inputField.textContent; + if (value.includes(okstring)) { + notificationBox.removeNotification(notification); + inputField.removeEventListener("keyup", pasteKeyUpHandler); + WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD; + } + } + inputField.addEventListener("keyup", pasteKeyUpHandler); + + event.preventDefault(); + event.stopPropagation(); + return false; + }; + return handler; + }, +}; + +exports.Utils = WebConsoleUtils; + +// Localization + +WebConsoleUtils.L10n = function (bundleURI) { + this._helper = new LocalizationHelper(bundleURI); +}; + +WebConsoleUtils.L10n.prototype = { + /** + * Generates a formatted timestamp string for displaying in console messages. + * + * @param integer [milliseconds] + * Optional, allows you to specify the timestamp in milliseconds since + * the UNIX epoch. + * @return string + * The timestamp formatted for display. + */ + timestampString: function (milliseconds) { + let d = new Date(milliseconds ? milliseconds : null); + let hours = d.getHours(), minutes = d.getMinutes(); + let seconds = d.getSeconds(); + milliseconds = d.getMilliseconds(); + let parameters = [hours, minutes, seconds, milliseconds]; + return this.getFormatStr("timestampFormat", parameters); + }, + + /** + * Retrieve a localized string. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @return string + * The localized string. + */ + getStr: function (name) { + try { + return this._helper.getStr(name); + } catch (ex) { + console.error("Failed to get string: " + name); + throw ex; + } + }, + + /** + * Retrieve a localized string formatted with values coming from the given + * array. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @param array array + * The array of values you want in the formatted string. + * @return string + * The formatted local string. + */ + getFormatStr: function (name, array) { + try { + return this._helper.getFormatStr(name, ...array); + } catch (ex) { + console.error("Failed to format string: " + name); + throw ex; + } + }, +}; diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js new file mode 100644 index 0000000000..bd7f90a0ec --- /dev/null +++ b/devtools/client/webconsole/webconsole.js @@ -0,0 +1,3658 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +const {Cc, Ci, Cu} = require("chrome"); + +const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = + require("devtools/client/webconsole/utils"); +const { getSourceNames } = require("devtools/client/shared/source-utils"); +const BrowserLoaderModule = {}; +Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule); + +const promise = require("promise"); +const Services = require("Services"); +const ErrorDocs = require("devtools/server/actors/errordocs"); +const Telemetry = require("devtools/client/shared/telemetry"); + +loader.lazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); +loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true); +loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true); +loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true); +loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true); +loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "system", "devtools/shared/system"); +loader.lazyRequireGetter(this, "JSTerm", "devtools/client/webconsole/jsterm", true); +loader.lazyRequireGetter(this, "gSequenceId", "devtools/client/webconsole/jsterm", true); +loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); +loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); +loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts", true); +loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys"); + +const {PluralForm} = require("devtools/shared/plural-form"); +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; +var 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/Web/Security/Mixed_content"; + +const IGNORED_SOURCE_URLS = ["debugger eval code"]; + +// The amount of time in milliseconds that we wait before performing a live +// search. +const SEARCH_DELAY = 200; + +// The number of lines that are displayed in the console output by default, for +// each category. The user can change this number by adjusting the hidden +// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences. +const DEFAULT_LOG_LIMIT = 1000; + +// The various categories of messages. We start numbering at zero so we can +// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below. +const CATEGORY_NETWORK = 0; +const CATEGORY_CSS = 1; +const CATEGORY_JS = 2; +const CATEGORY_WEBDEV = 3; +// always on +const CATEGORY_INPUT = 4; +// always on +const CATEGORY_OUTPUT = 5; +const CATEGORY_SECURITY = 6; +const CATEGORY_SERVER = 7; + +// The possible message severities. As before, we start at zero so we can use +// these as indexes into MESSAGE_PREFERENCE_KEYS. +const SEVERITY_ERROR = 0; +const SEVERITY_WARNING = 1; +const SEVERITY_INFO = 2; +const SEVERITY_LOG = 3; + +// The fragment of a CSS class name that identifies each category. +const CATEGORY_CLASS_FRAGMENTS = [ + "network", + "cssparser", + "exception", + "console", + "input", + "output", + "security", + "server", +]; + +// The fragment of a CSS class name that identifies each severity. +const SEVERITY_CLASS_FRAGMENTS = [ + "error", + "warn", + "info", + "log", +]; + +// The preference keys to use for each category/severity combination, indexed +// first by category (rows) and then by severity (columns) in the following +// order: +// +// [ Error, Warning, Info, Log ] +// +// Most of these rather idiosyncratic names are historical and predate the +// division of message type into "category" and "severity". +const MESSAGE_PREFERENCE_KEYS = [ + // Network + [ "network", "netwarn", "netxhr", "networkinfo", ], + // CSS + [ "csserror", "cssparser", null, "csslog", ], + // JS + [ "exception", "jswarn", null, "jslog", ], + // Web Developer + [ "error", "warn", "info", "log", ], + // Input + [ null, null, null, null, ], + // Output + [ null, null, null, null, ], + // Security + [ "secerror", "secwarn", null, null, ], + // Server Logging + [ "servererror", "serverwarn", "serverinfo", "serverlog", ], +]; + +// 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, + clear: SEVERITY_LOG, + trace: SEVERITY_LOG, + table: SEVERITY_LOG, + debug: SEVERITY_LOG, + dir: SEVERITY_LOG, + dirxml: SEVERITY_LOG, + group: SEVERITY_LOG, + groupCollapsed: SEVERITY_LOG, + groupEnd: SEVERITY_LOG, + time: SEVERITY_LOG, + timeEnd: SEVERITY_LOG, + count: SEVERITY_LOG +}; + +// This array contains the prefKey for the workers and it must keep them in the +// same order as CONSOLE_WORKER_IDS +const WORKERTYPES_PREFKEYS = + [ "sharedworkers", "serviceworkers", "windowlessworkers" ]; + +// The lowest HTTP response code (inclusive) that is considered an error. +const MIN_HTTP_ERROR_CODE = 400; +// The highest HTTP response code (inclusive) that is considered an error. +const MAX_HTTP_ERROR_CODE = 599; + +// The indent of a console group in pixels. +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 down the Firefox UI too much. +const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; + +// The delay (in milliseconds) 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 = 20; + +// The maximum amount of time (in milliseconds) 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; + +// 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 +// update every N milliseconds given here. +const THROTTLE_UPDATES = 1000; + +// The preference prefix for all of the Web Console filters. +const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; + +// The minimum font size. +const MIN_FONT_SIZE = 10; + +const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; +const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; +const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; +const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled"; + +/** + * A WebConsoleFrame instance is an interactive console initialized *per target* + * that displays console log data as well as provides an interactive terminal to + * manipulate the target's document content. + * + * The WebConsoleFrame is responsible for the actual Web Console UI + * implementation. + * + * @constructor + * @param object webConsoleOwner + * The WebConsole owner object. + */ +function WebConsoleFrame(webConsoleOwner) { + this.owner = webConsoleOwner; + this.hudId = this.owner.hudId; + this.isBrowserConsole = this.owner._browserConsole; + + this.window = this.owner.iframeWindow; + + this._repeatNodes = {}; + this._outputQueue = []; + this._itemDestroyQueue = []; + this._pruneCategoriesQueue = {}; + this.filterPrefs = {}; + + this.output = new ConsoleOutput(this); + + this.unmountMessage = this.unmountMessage.bind(this); + this._toggleFilter = this._toggleFilter.bind(this); + this.resize = this.resize.bind(this); + this._onPanelSelected = this._onPanelSelected.bind(this); + this._flushMessageQueue = this._flushMessageQueue.bind(this); + this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); + this._onUpdateListeners = this._onUpdateListeners.bind(this); + + this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._outputTimerInitialized = false; + + let require = BrowserLoaderModule.BrowserLoader({ + window: this.window, + useOnlyShared: true + }).require; + + this.React = require("devtools/client/shared/vendor/react"); + this.ReactDOM = require("devtools/client/shared/vendor/react-dom"); + this.FrameView = this.React.createFactory(require("devtools/client/shared/components/frame")); + this.StackTraceView = this.React.createFactory(require("devtools/client/shared/components/stack-trace")); + + this._telemetry = new Telemetry(); + + EventEmitter.decorate(this); +} +exports.WebConsoleFrame = WebConsoleFrame; + +WebConsoleFrame.prototype = { + /** + * The WebConsole instance that owns this frame. + * @see hudservice.js::WebConsole + * @type object + */ + owner: null, + + /** + * Proxy between the Web Console and the remote Web Console instance. This + * object holds methods used for connecting, listening and disconnecting from + * the remote server, using the remote debugging protocol. + * + * @see WebConsoleConnectionProxy + * @type object + */ + proxy: null, + + /** + * Getter for the xul:popupset that holds any popups we open. + * @type nsIDOMElement + */ + get popupset() { + return this.owner.mainPopupSet; + }, + + /** + * Holds the initialization promise object. + * @private + * @type object + */ + _initDefer: null, + + /** + * Last time when we displayed any message in the output. + * + * @private + * @type number + * Timestamp in milliseconds since the Unix epoch. + */ + _lastOutputFlush: 0, + + /** + * Message nodes are stored here in a queue for later display. + * + * @private + * @type array + */ + _outputQueue: null, + + /** + * Keep track of the categories we need to prune from time to time. + * + * @private + * @type array + */ + _pruneCategoriesQueue: null, + + /** + * Function invoked whenever the output queue is emptied. This is used by some + * tests. + * + * @private + * @type function + */ + _flushCallback: null, + + /** + * Timer used for flushing the messages output queue. + * + * @private + * @type nsITimer + */ + _outputTimer: null, + _outputTimerInitialized: null, + + /** + * Store for tracking repeated nodes. + * @private + * @type object + */ + _repeatNodes: null, + + /** + * Preferences for filtering messages by type. + * @see this._initDefaultFilterPrefs() + * @type object + */ + filterPrefs: null, + + /** + * Prefix used for filter preferences. + * @private + * @type string + */ + _filterPrefsPrefix: FILTER_PREFS_PREFIX, + + /** + * The nesting depth of the currently active console group. + */ + groupDepth: 0, + + /** + * The current target location. + * @type string + */ + contentLocation: "", + + /** + * The JSTerm object that manage the console's input. + * @see JSTerm + * @type object + */ + jsterm: null, + + /** + * The element that holds all of the messages we display. + * @type nsIDOMElement + */ + 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 + */ + filterBox: null, + + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() { + return this.proxy ? this.proxy.webConsoleClient : null; + }, + + _destroyer: null, + + _saveRequestAndResponseBodies: true, + _throttleData: null, + + // 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, + + /** + * Setter for saving of network request and response bodies. + * + * @param boolean value + * The new value you want to set. + */ + setSaveRequestAndResponseBodies: function (value) { + if (!this.webConsoleClient) { + // Don't continue if the webconsole disconnected. + return promise.resolve(null); + } + + let deferred = promise.defer(); + let newValue = !!value; + let toSet = { + "NetworkMonitor.saveRequestAndResponseBodies": newValue, + }; + + // Make sure the web console client connection is established first. + this.webConsoleClient.setPreferences(toSet, response => { + if (!response.error) { + this._saveRequestAndResponseBodies = newValue; + deferred.resolve(response); + } else { + deferred.reject(response.error); + } + }); + + return deferred.promise; + }, + + /** + * Setter for throttling data. + * + * @param boolean value + * The new value you want to set; @see NetworkThrottleManager. + */ + setThrottleData: function(value) { + if (!this.webConsoleClient) { + // Don't continue if the webconsole disconnected. + return promise.resolve(null); + } + + let deferred = promise.defer(); + let toSet = { + "NetworkMonitor.throttleData": value, + }; + + // Make sure the web console client connection is established first. + this.webConsoleClient.setPreferences(toSet, response => { + if (!response.error) { + this._throttleData = value; + deferred.resolve(response); + } else { + deferred.reject(response.error); + } + }); + + return deferred.promise; + }, + + /** + * Getter for the persistent logging preference. + * @type boolean + */ + get persistLog() { + // For the browser console, we receive tab navigation + // when the original top level window we attached to is closed, + // but we don't want to reset console history and just switch to + // the next available window. + return this.isBrowserConsole || + Services.prefs.getBoolPref(PREF_PERSISTLOG); + }, + + /** + * Initialize the WebConsoleFrame instance. + * @return object + * A promise object that resolves once the frame is ready to use. + */ + init: function () { + this._initUI(); + let connectionInited = this._initConnection(); + + // Don't reject if the history fails to load for some reason. + // This would be fine, the panel will just start with empty history. + let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => { + return connectionInited; + }); + + // This notification is only used in tests. Don't chain it onto + // the returned promise because the console panel needs to be attached + // to the toolbox before the web-console-created event is receieved. + let notifyObservers = () => { + let id = WebConsoleUtils.supportsString(this.hudId); + Services.obs.notifyObservers(id, "web-console-created", null); + }; + allReady.then(notifyObservers, notifyObservers); + + if (this.NEW_CONSOLE_OUTPUT_ENABLED) { + allReady.then(this.newConsoleOutput.init); + } + + return allReady; + }, + + /** + * Connect to the server using the remote debugging protocol. + * + * @private + * @return object + * A promise object that is resolved/reject based on the connection + * result. + */ + _initConnection: function () { + if (this._initDefer) { + return this._initDefer.promise; + } + + this._initDefer = promise.defer(); + this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); + + this.proxy.connect().then(() => { + // on success + this._initDefer.resolve(this); + }, (reason) => { + // on failure + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, + reason.error + ": " + reason.message); + this.outputMessage(CATEGORY_JS, node, [reason]); + this._initDefer.reject(reason); + }); + + return this._initDefer.promise; + }, + + /** + * Find the Web Console UI elements and setup event listeners as needed. + * @private + */ + _initUI: function () { + this.document = this.window.document; + this.rootElement = this.document.documentElement; + this.NEW_CONSOLE_OUTPUT_ENABLED = !this.isBrowserConsole + && !this.owner.target.chrome + && Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED); + + this.outputNode = this.document.getElementById("output-container"); + this.outputWrapper = this.document.getElementById("output-wrapper"); + this.completeNode = this.document.querySelector(".jsterm-complete-node"); + this.inputNode = this.document.querySelector(".jsterm-input-node"); + + // In the old frontend, the area that scrolls is outputWrapper, but in the new + // frontend this will be reassigned. + this.outputScroller = this.outputWrapper; + + // Update the character width and height needed for the popup offset + // calculations. + this._updateCharSize(); + + this.jsterm = new JSTerm(this); + this.jsterm.init(); + + let toolbox = gDevTools.getToolbox(this.owner.target); + + if (this.NEW_CONSOLE_OUTPUT_ENABLED) { + // @TODO Remove this once JSTerm is handled with React/Redux. + this.window.jsterm = this.jsterm; + + // Remove context menu for now (see Bug 1307239). + this.outputWrapper.removeAttribute("context"); + + // XXX: We should actually stop output from happening on old output + // panel, but for now let's just hide it. + this.experimentalOutputNode = this.outputNode.cloneNode(); + this.experimentalOutputNode.removeAttribute("tabindex"); + this.outputNode.hidden = true; + this.outputNode.parentNode.appendChild(this.experimentalOutputNode); + // @TODO Once the toolbox has been converted to React, see if passing + // in JSTerm is still necessary. + + this.newConsoleOutput = new this.window.NewConsoleOutput( + this.experimentalOutputNode, this.jsterm, toolbox, this.owner, this.document); + + let filterToolbar = this.document.querySelector(".hud-console-filter-toolbar"); + filterToolbar.hidden = true; + } else { + // Register the controller to handle "select all" properly. + this._commandController = new CommandController(this); + this.window.controllers.insertControllerAt(0, this._commandController); + + this._contextMenuHandler = new ConsoleContextMenu(this); + + this._initDefaultFilterPrefs(); + this.filterBox = this.document.querySelector(".hud-filter-box"); + this._setFilterTextBoxEvents(); + this._initFilterButtons(); + let clearButton = + this.document.getElementsByClassName("webconsole-clear-console-button")[0]; + clearButton.addEventListener("command", () => { + this.owner._onClearButton(); + this.jsterm.clearOutput(true); + }); + + } + + this.resize(); + this.window.addEventListener("resize", this.resize, true); + this.jsterm.on("sidebar-opened", this.resize); + this.jsterm.on("sidebar-closed", this.resize); + + if (toolbox) { + toolbox.on("webconsole-selected", this._onPanelSelected); + } + + /* + * Focus the input line whenever the output area is clicked. + */ + this.outputWrapper.addEventListener("click", (event) => { + // Do not focus on middle/right-click or 2+ clicks. + if (event.detail !== 1 || event.button !== 0) { + return; + } + + // Do not focus if something is selected + let selection = this.window.getSelection(); + if (selection && !selection.isCollapsed) { + return; + } + + // Do not focus if a link was clicked + if (event.target.nodeName.toLowerCase() === "a" || + event.target.parentNode.nodeName.toLowerCase() === "a") { + return; + } + + // Do not focus if a search input was clicked on the new frontend + if (this.NEW_CONSOLE_OUTPUT_ENABLED && + event.target.nodeName.toLowerCase() === "input" && + event.target.getAttribute("type").toLowerCase() === "search") { + return; + } + + this.jsterm.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), + }); + + this._initShortcuts(); + + // focus input node + this.jsterm.focus(); + }, + + /** + * Resizes the output node to fit the output wrapped. + * We need this because it makes the layout a lot faster than + * using -moz-box-flex and 100% width. See Bug 1237368. + */ + resize: function () { + if (this.NEW_CONSOLE_OUTPUT_ENABLED) { + this.experimentalOutputNode.style.width = + this.outputWrapper.clientWidth + "px"; + } else { + this.outputNode.style.width = this.outputWrapper.clientWidth + "px"; + } + }, + + /** + * 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 () { + this.jsterm.focus(); + }, + + /** + * Initialize the default filter preferences. + * @private + */ + _initDefaultFilterPrefs: function () { + let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", + "exception", "jswarn", "jslog", "error", "info", "warn", "log", + "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers", + "serviceworkers", "windowlessworkers", "servererror", + "serverwarn", "serverinfo", "serverlog"]; + + for (let pref of prefs) { + this.filterPrefs[pref] = Services.prefs.getBoolPref( + this._filterPrefsPrefix + pref); + } + }, + + _initShortcuts: function() { + var shortcuts = new KeyShortcuts({ + window: this.window + }); + + shortcuts.on(l10n.getStr("webconsole.find.key"), + (name, event) => { + this.filterBox.focus(); + event.preventDefault(); + }); + + let clearShortcut; + if (system.constants.platform === "macosx") { + clearShortcut = l10n.getStr("webconsole.clear.keyOSX"); + } else { + clearShortcut = l10n.getStr("webconsole.clear.key"); + } + shortcuts.on(clearShortcut, + () => this.jsterm.clearOutput(true)); + + if (this.isBrowserConsole) { + shortcuts.on(l10n.getStr("webconsole.close.key"), + this.window.close.bind(this.window)); + + ZoomKeys.register(this.window); + } + }, + + /** + * Attach / detach reflow listeners depending on the checked status + * of the `CSS > Log` menuitem. + * + * @param function [callback=null] + * Optional function to invoke when the listener has been + * added/removed. + */ + _updateReflowActivityListener: function (callback) { + if (this.webConsoleClient) { + let pref = this._filterPrefsPrefix + "csslog"; + if (Services.prefs.getBoolPref(pref)) { + this.webConsoleClient.startListeners(["ReflowActivity"], callback); + } else { + this.webConsoleClient.stopListeners(["ReflowActivity"], callback); + } + } + }, + + /** + * Attach / detach server logging listener depending on the filter + * preferences. If the user isn't interested in the server logs at + * all the listener is not registered. + * + * @param function [callback=null] + * Optional function to invoke when the listener has been + * added/removed. + */ + _updateServerLoggingListener: function (callback) { + if (!this.webConsoleClient) { + return null; + } + + let startListener = false; + let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"]; + for (let i = 0; i < prefs.length; i++) { + if (this.filterPrefs[prefs[i]]) { + startListener = true; + break; + } + } + + if (startListener) { + this.webConsoleClient.startListeners(["ServerLogging"], callback); + } else { + this.webConsoleClient.stopListeners(["ServerLogging"], callback); + } + }, + + /** + * Sets the events for the filter input field. + * @private + */ + _setFilterTextBoxEvents: function () { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this); + + let onChange = function _onChange() { + // To improve responsiveness, we let the user finish typing before we + // perform the search. + timer.cancel(); + timer.initWithCallback(timerEvent, SEARCH_DELAY, + Ci.nsITimer.TYPE_ONE_SHOT); + }; + + this.filterBox.addEventListener("command", onChange, false); + this.filterBox.addEventListener("input", onChange, false); + }, + + /** + * Creates one of the filter buttons on the toolbar. + * + * @private + * @param nsIDOMNode aParent + * The node to which the filter button should be appended. + * @param object aDescriptor + * A descriptor that contains info about the button. Contains "name", + * "category", and "prefKey" properties, and optionally a "severities" + * property. + */ + _initFilterButtons: function () { + let categories = this.document + .querySelectorAll(".webconsole-filter-button[category]"); + Array.forEach(categories, function (button) { + button.addEventListener("contextmenu", () => { + button.open = true; + }, false); + button.addEventListener("click", this._toggleFilter, false); + + let someChecked = false; + let severities = button.querySelectorAll("menuitem[prefKey]"); + Array.forEach(severities, function (menuItem) { + menuItem.addEventListener("command", this._toggleFilter, false); + + let prefKey = menuItem.getAttribute("prefKey"); + let checked = this.filterPrefs[prefKey]; + menuItem.setAttribute("checked", checked); + someChecked = someChecked || checked; + }, this); + + button.setAttribute("checked", someChecked); + button.setAttribute("aria-pressed", someChecked); + }, this); + + if (!this.isBrowserConsole) { + // The Browser Console displays nsIConsoleMessages which are messages that + // end up in the JS category, but they are not errors or warnings, they + // are just log messages. The Web Console does not show such messages. + 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"); + + let serverLogging = + this.document.querySelector("toolbarbutton[category=server]"); + serverLogging.removeAttribute("accesskey"); + } + }, + + /** + * 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 () { + 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; + }, + + /** + * The event handler that is called whenever a user switches a filter on or + * off. + * + * @private + * @param nsIDOMEvent event + * The event that triggered the filter change. + */ + _toggleFilter: function (event) { + let target = event.target; + let tagName = target.tagName; + // Prevent toggle if generated from a contextmenu event (right click) + let isRightClick = (event.button === 2); + if (tagName != event.currentTarget.tagName || isRightClick) { + return; + } + + switch (tagName) { + case "toolbarbutton": { + let originalTarget = event.originalTarget; + let classes = originalTarget.classList; + + if (originalTarget.localName !== "toolbarbutton") { + // Oddly enough, the click event is sent to the menu button when + // selecting a menu item with the mouse. Detect this case and bail + // out. + break; + } + + if (!classes.contains("toolbarbutton-menubutton-button") && + originalTarget.getAttribute("type") === "menu-button") { + // This is a filter button with a drop-down. The user clicked the + // drop-down, so do nothing. (The menu will automatically appear + // without our intervention.) + 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 (event.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. + 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; + } + + case "menuitem": { + let state = target.getAttribute("checked") !== "true"; + target.setAttribute("checked", state); + + let prefKey = target.getAttribute("prefKey"); + this.setFilterState(prefKey, state); + + // Adjust the state of the button appropriately. + let menuPopup = target.parentNode; + + let someChecked = false; + let menuItem = menuPopup.firstChild; + while (menuItem) { + if (menuItem.hasAttribute("prefKey") && + menuItem.getAttribute("checked") === "true") { + someChecked = true; + break; + } + menuItem = menuItem.nextSibling; + } + 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 target + * Button with drop down items to be toggled. + * @param boolean state + * True if the menu item is being toggled on, and false otherwise. + */ + _setMenuState: function (target, state) { + let menuItems = target.querySelectorAll("menuitem"); + Array.forEach(menuItems, (item) => { + item.setAttribute("checked", state); + let prefKey = item.getAttribute("prefKey"); + this.setFilterState(prefKey, state); + }); + }, + + /** + * Set the filter state for a specific toggle button. + * + * @param string toggleType + * @param boolean state + * @returns void + */ + setFilterState: function (toggleType, state) { + this.filterPrefs[toggleType] = state; + this.adjustVisibilityForMessageType(toggleType, state); + + Services.prefs.setBoolPref(this._filterPrefsPrefix + toggleType, state); + + if (this._updateListenersTimeout) { + clearTimeout(this._updateListenersTimeout); + } + + this._updateListenersTimeout = setTimeout( + this._onUpdateListeners, 200); + }, + + /** + * Get the filter state for a specific toggle button. + * + * @param string toggleType + * @returns boolean + */ + getFilterState: function (toggleType) { + return this.filterPrefs[toggleType]; + }, + + /** + * Called when a logging filter changes. Allows to stop/start + * listeners according to the current filter state. + */ + _onUpdateListeners: function () { + this._updateReflowActivityListener(); + this._updateServerLoggingListener(); + }, + + /** + * Check that the passed string matches the filter arguments. + * + * @param String str + * to search for filter words in. + * @param String filter + * is a string containing all of the words to filter on. + * @returns boolean + */ + stringMatchesFilters: function (str, filter) { + if (!filter || !str) { + return true; + } + + let searchStr = str.toLowerCase(); + let filterStrings = filter.toLowerCase().split(/\s+/); + return !filterStrings.some(function (f) { + return searchStr.indexOf(f) == -1; + }); + }, + + /** + * Turns the display of log nodes on and off appropriately to reflect the + * adjustment of the message type filter named by @prefKey. + * + * @param string prefKey + * The preference key for the message type being filtered: one of the + * values in the MESSAGE_PREFERENCE_KEYS table. + * @param boolean state + * True if the filter named by @messageType is being turned on; false + * otherwise. + * @returns void + */ + adjustVisibilityForMessageType: function (prefKey, state) { + let outputNode = this.outputNode; + let doc = this.document; + + // 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 attribute = WORKERTYPES_PREFKEYS.indexOf(prefKey) == -1 + ? "filter" : "workerType"; + + let xpath = ".//*[contains(@class, 'message') and " + + "@" + attribute + "='" + prefKey + "']"; + 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 (state) { + node.classList.remove("filtered-by-type"); + } else { + node.classList.add("filtered-by-type"); + } + } + }, + + /** + * Turns the display of log nodes on and off appropriately to reflect the + * adjustment of the search string. + */ + adjustVisibilityOnSearchStringChange: function () { + 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.textContent; + + // if the text matches the words in aSearchString... + if (this.stringMatchesFilters(text, searchString)) { + node.classList.remove("filtered-by-string"); + } else { + node.classList.add("filtered-by-string"); + } + } + + this.resize(); + }, + + /** + * Applies the user's filters to a newly-created message node via CSS + * classes. + * + * @param nsIDOMNode node + * The newly-created message node. + * @return boolean + * True if the message was filtered or false otherwise. + */ + filterMessageNode: function (node) { + let isFiltered = false; + + // Filter by the message type. + let prefKey = MESSAGE_PREFERENCE_KEYS[node.category][node.severity]; + if (prefKey && !this.getFilterState(prefKey)) { + // The node is filtered by type. + node.classList.add("filtered-by-type"); + isFiltered = true; + } + + // Filter by worker type + if ("workerType" in node && !this.getFilterState(node.workerType)) { + node.classList.add("filtered-by-type"); + isFiltered = true; + } + + // Filter on the search string. + let search = this.filterBox.value; + let text = node.clipboardText; + + // if string matches the filter text + if (!this.stringMatchesFilters(text, search)) { + node.classList.add("filtered-by-string"); + isFiltered = true; + } + + if (isFiltered && node.classList.contains("inlined-variables-view")) { + node.classList.add("hidden-message"); + } + + return isFiltered; + }, + + /** + * Merge the attributes of repeated nodes. + * + * @param nsIDOMNode original + * The Original Node. The one being merged into. + */ + mergeFilteredMessageNode: function (original) { + let repeatNode = original.getElementsByClassName("message-repeats")[0]; + if (!repeatNode) { + // no repeat node, return early. + return; + } + + let occurrences = parseInt(repeatNode.getAttribute("value"), 10) + 1; + repeatNode.setAttribute("value", occurrences); + repeatNode.textContent = occurrences; + let str = l10n.getStr("messageRepeats.tooltip2"); + repeatNode.title = PluralForm.get(occurrences, str) + .replace("#1", occurrences); + }, + + /** + * Filter the message node from the output if it is a repeat. + * + * @private + * @param nsIDOMNode node + * The message node to be filtered or not. + * @returns nsIDOMNode|null + * Returns the duplicate node if the message was filtered, null + * otherwise. + */ + _filterRepeatedMessage: function (node) { + let repeatNode = node.getElementsByClassName("message-repeats")[0]; + if (!repeatNode) { + return null; + } + + let uid = repeatNode._uid; + let dupeNode = null; + + if (node.category == CATEGORY_CSS || + node.category == CATEGORY_SECURITY) { + dupeNode = this._repeatNodes[uid]; + if (!dupeNode) { + this._repeatNodes[uid] = node; + } + } else if ((node.category == CATEGORY_WEBDEV || + node.category == CATEGORY_JS) && + node.category != CATEGORY_NETWORK && + !node.classList.contains("inlined-variables-view")) { + let lastMessage = this.outputNode.lastChild; + if (!lastMessage) { + return null; + } + + let lastRepeatNode = + lastMessage.getElementsByClassName("message-repeats")[0]; + if (lastRepeatNode && lastRepeatNode._uid == uid) { + dupeNode = lastMessage; + } + } + + if (dupeNode) { + this.mergeFilteredMessageNode(dupeNode); + // Even though this node was never rendered, we create the location + // nodes before rendering, so we still have to clean up any + // React components + this.unmountMessage(node); + return dupeNode; + } + + return null; + }, + + /** + * Display cached messages that may have been collected before the UI is + * displayed. + * + * @param array remoteMessages + * Array of cached messages coming from the remote Web Console + * content instance. + */ + displayCachedMessages: function (remoteMessages) { + if (!remoteMessages.length) { + return; + } + + remoteMessages.forEach(function (message) { + switch (message._type) { + case "PageError": { + let category = Utils.categoryForScriptError(message); + this.outputMessage(category, this.reportPageError, + [category, message]); + break; + } + case "LogMessage": + this.handleLogMessage(message); + break; + case "ConsoleAPI": + this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, + [message]); + break; + case "NetworkEvent": + this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [message]); + break; + } + }, this); + }, + + /** + * Logs a message to the Web Console that originates from the Web Console + * server. + * + * @param object message + * The message received from the server. + * @return nsIDOMElement|null + * The message element to display in the Web Console output. + */ + logConsoleAPIMessage: function (message) { + let body = null; + let clipboardText = null; + let sourceURL = message.filename; + let sourceLine = message.lineNumber; + let level = message.level; + let args = message.arguments; + let objectActors = new Set(); + let node = null; + + // Gather the actor IDs. + args.forEach((value) => { + if (WebConsoleUtils.isActorGrip(value)) { + objectActors.add(value.actor); + } + }); + + switch (level) { + case "log": + case "info": + case "warn": + case "error": + case "exception": + case "assert": + case "debug": { + let msg = new Messages.ConsoleGeneric(message); + node = msg.init(this.output).render().element; + break; + } + case "table": { + let msg = new Messages.ConsoleTable(message); + node = msg.init(this.output).render().element; + break; + } + case "trace": { + let msg = new Messages.ConsoleTrace(message); + node = msg.init(this.output).render().element; + break; + } + case "clear": { + body = l10n.getStr("consoleCleared"); + clipboardText = body; + break; + } + case "dir": { + body = { arguments: args }; + let clipboardArray = []; + args.forEach((value) => { + clipboardArray.push(VariablesView.getString(value)); + }); + clipboardText = clipboardArray.join(" "); + break; + } + case "dirxml": { + // We just alias console.dirxml() with console.log(). + message.level = "log"; + return this.logConsoleAPIMessage(message); + } + case "group": + case "groupCollapsed": + clipboardText = body = message.groupName; + this.groupDepth++; + break; + + case "groupEnd": + if (this.groupDepth > 0) { + this.groupDepth--; + } + break; + + case "time": { + let timer = message.timer; + if (!timer) { + return null; + } + if (timer.error) { + console.error(new Error(l10n.getStr(timer.error))); + return null; + } + body = l10n.getFormatStr("timerStarted", [timer.name]); + clipboardText = body; + break; + } + + case "timeEnd": { + let timer = message.timer; + if (!timer) { + return null; + } + let duration = Math.round(timer.duration * 100) / 100; + body = l10n.getFormatStr("timeEnd", [timer.name, duration]); + clipboardText = body; + break; + } + + case "count": { + let counter = message.counter; + if (!counter) { + return null; + } + if (counter.error) { + console.error(l10n.getStr(counter.error)); + return null; + } + let msg = new Messages.ConsoleGeneric(message); + node = msg.init(this.output).render().element; + break; + } + + case "timeStamp": { + // console.timeStamp() doesn't need to display anything. + return null; + } + + default: + console.error(new Error("Unknown Console API log level: " + level)); + return null; + } + + // Release object actors for arguments coming from console API methods that + // we ignore their arguments. + switch (level) { + case "group": + case "groupCollapsed": + case "groupEnd": + case "time": + case "timeEnd": + case "count": + for (let actor of objectActors) { + this._releaseObject(actor); + } + objectActors.clear(); + } + + if (level == "groupEnd") { + // no need to continue + return null; + } + + if (!node) { + node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, + sourceURL, sourceLine, clipboardText, + level, message.timeStamp); + if (message.private) { + node.setAttribute("private", true); + } + } + + if (objectActors.size > 0) { + node._objectActors = objectActors; + + if (!node._messageObject) { + let repeatNode = node.getElementsByClassName("message-repeats")[0]; + repeatNode._uid += [...objectActors].join("-"); + } + } + + let workerTypeID = CONSOLE_WORKER_IDS.indexOf(message.workerType); + if (workerTypeID != -1) { + node.workerType = WORKERTYPES_PREFKEYS[workerTypeID]; + node.setAttribute("workerType", WORKERTYPES_PREFKEYS[workerTypeID]); + } + + return node; + }, + + /** + * Handle ConsoleAPICall objects received from the server. This method outputs + * the window.console API call. + * + * @param object message + * The console API message received from the server. + */ + handleConsoleAPICall: function (message) { + this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [message]); + }, + + /** + * Reports an error in the page source, either JavaScript or CSS. + * + * @param nsIScriptError scriptError + * The error message to report. + * @return nsIDOMElement|undefined + * The message element to display in the Web Console output. + */ + reportPageError: function (category, scriptError) { + // Warnings and legacy strict errors become warnings; other types become + // errors. + let severity = "error"; + if (scriptError.warning || scriptError.strict) { + severity = "warning"; + } else if (scriptError.info) { + severity = "log"; + } + + switch (category) { + case CATEGORY_CSS: + category = "css"; + break; + case CATEGORY_SECURITY: + category = "security"; + break; + default: + category = "js"; + break; + } + + let objectActors = new Set(); + + // Gather the actor IDs. + for (let prop of ["errorMessage", "lineText"]) { + let grip = scriptError[prop]; + if (WebConsoleUtils.isActorGrip(grip)) { + objectActors.add(grip.actor); + } + } + + let errorMessage = scriptError.errorMessage; + if (errorMessage.type && errorMessage.type == "longString") { + errorMessage = errorMessage.initial; + } + + let displayOrigin = scriptError.sourceName; + + // TLS errors are related to the connection and not the resource; therefore + // it makes sense to only display the protcol, host and port (prePath). + // This also means messages are grouped for a single origin. + if (scriptError.category && scriptError.category == "SHA-1 Signature") { + let sourceURI = Services.io.newURI(scriptError.sourceName, null, null) + .QueryInterface(Ci.nsIURL); + displayOrigin = sourceURI.prePath; + } + + // Create a new message + let msg = new Messages.Simple(errorMessage, { + location: { + url: displayOrigin, + line: scriptError.lineNumber, + column: scriptError.columnNumber + }, + stack: scriptError.stacktrace, + category: category, + severity: severity, + timestamp: scriptError.timeStamp, + private: scriptError.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 + if (scriptError.exceptionDocURL) { + this.addLearnMoreWarningNode(msgBody, scriptError.exceptionDocURL); + } + + // Collect telemetry data regarding JavaScript errors + this._telemetry.logKeyed("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED", + scriptError.errorMessageName, + true); + + if (objectActors.size > 0) { + node._objectActors = objectActors; + } + + return node; + }, + + /** + * Handle PageError objects received from the server. This method outputs the + * given error. + * + * @param nsIScriptError pageError + * The error received from the server. + */ + handlePageError: function (pageError) { + let category = Utils.categoryForScriptError(pageError); + this.outputMessage(category, this.reportPageError, [category, pageError]); + }, + + /** + * Handle log messages received from the server. This method outputs the given + * message. + * + * @param object packet + * The message packet received from the server. + */ + handleLogMessage: function (packet) { + if (packet.message) { + this.outputMessage(CATEGORY_JS, this._reportLogMessage, [packet]); + } + }, + + /** + * Display log messages received from the server. + * + * @private + * @param object packet + * The message packet received from the server. + * @return nsIDOMElement + * The message element to render for the given log message. + */ + _reportLogMessage: function (packet) { + let msg = packet.message; + if (msg.type && msg.type == "longString") { + msg = msg.initial; + } + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null, + null, null, null, packet.timeStamp); + if (WebConsoleUtils.isActorGrip(packet.message)) { + node._objectActors = new Set([packet.message.actor]); + } + return node; + }, + + /** + * Log network event. + * + * @param object networkInfo + * The network request information to log. + * @return nsIDOMElement|null + * The message element to display in the Web Console output. + */ + logNetEvent: function (networkInfo) { + let actorId = networkInfo.actor; + let request = networkInfo.request; + 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) { + severity = SEVERITY_WARNING; + } + + let methodNode = this.document.createElementNS(XHTML_NS, "span"); + methodNode.className = "method"; + methodNode.textContent = request.method + " "; + + let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, + methodNode, null, null, + clipboardText, null, + networkInfo.timeStamp); + if (networkInfo.private) { + messageNode.setAttribute("private", true); + } + messageNode._connectionId = actorId; + messageNode.url = request.url; + + 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 = () => this.openNetworkPanel(networkInfo.actor); + + this._addMessageLinkCallback(urlNode, onClick); + this._addMessageLinkCallback(statusNode, onClick); + + networkInfo.node = messageNode; + + this._updateNetMessage(actorId); + + if (this.window.NetRequest) { + this.window.NetRequest.onNetworkEvent({ + consoleFrame: this, + response: networkInfo, + node: messageNode, + update: false + }); + } + + return messageNode; + }, + + /** + * Create a mixed content warning Node. + * + * @param linkNode + * Parent to the requested urlNode. + */ + makeMixedContentNode: function (linkNode) { + let mixedContentWarning = + "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; + + // Mixed content warning message links to a Learn More page + 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; + + linkNode.appendChild(mixedContentWarningNode); + + this._addMessageLinkCallback(mixedContentWarningNode, (event) => { + event.stopPropagation(); + this.owner.openLink(MIXED_CONTENT_LEARN_MORE); + }); + }, + + /* + * 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 node + * The node to which we will be adding a clickable warning node. + * @param url + * 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 (node, url) { + let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]"; + + let warningNode = this.document.createElementNS(XHTML_NS, "a"); + warningNode.title = url.split("?")[0]; + warningNode.href = url; + warningNode.draggable = false; + warningNode.textContent = moreInfoLabel; + warningNode.className = "learn-more-link"; + + this._addMessageLinkCallback(warningNode, (event) => { + event.stopPropagation(); + this.owner.openLink(url); + }); + + node.appendChild(warningNode); + }, + + /** + * Log file activity. + * + * @param string fileURI + * The file URI that was loaded. + * @return nsIDOMElement|undefined + * The message element to display in the Web Console output. + */ + logFileActivity: function (fileURI) { + let urlNode = this.document.createElementNS(XHTML_NS, "a"); + urlNode.setAttribute("title", fileURI); + urlNode.className = "url"; + urlNode.textContent = fileURI; + urlNode.draggable = false; + urlNode.href = fileURI; + + let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, + urlNode, null, null, fileURI); + + this._addMessageLinkCallback(urlNode, () => { + this.owner.viewSource(fileURI); + }); + + return outputNode; + }, + + /** + * Handle the file activity messages coming from the remote Web Console. + * + * @param string fileURI + * The file URI that was requested. + */ + handleFileActivity: function (fileURI) { + this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [fileURI]); + }, + + /** + * Handle the reflow activity messages coming from the remote Web Console. + * + * @param object msg + * An object holding information about a reflow batch. + */ + logReflowActivity: function (message) { + let {start, end, sourceURL, sourceLine} = message; + 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 = getSourceNames(sourceURL).short; + let functionName = message.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 (message) { + this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]); + }, + + /** + * Inform user that the window.console API has been replaced by a script + * in a content page. + */ + logWarningAboutReplacedAPI: function () { + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, + l10n.getStr("ConsoleAPIDisabled")); + this.outputMessage(CATEGORY_JS, node); + }, + + /** + * Handle the network events coming from the remote Web Console. + * + * @param object networkInfo + * The network request information. + */ + handleNetworkEvent: function (networkInfo) { + this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]); + }, + + /** + * Handle network event updates coming from the server. + * + * @param object networkInfo + * The network request information. + * @param object packet + * Update details. + */ + handleNetworkEventUpdate: function (networkInfo, packet) { + if (networkInfo.node && this._updateNetMessage(packet.from)) { + if (this.window.NetRequest) { + this.window.NetRequest.onNetworkEvent({ + client: this.webConsoleClient, + response: packet, + node: networkInfo.node, + update: true + }); + } + + this.emit("new-messages", new Set([{ + update: true, + node: networkInfo.node, + response: packet, + }])); + } + + // For unit tests we pass the HTTP activity object to the test callback, + // once requests complete. + if (this.owner.lastFinishedRequestCallback && + networkInfo.updates.indexOf("responseContent") > -1 && + networkInfo.updates.indexOf("eventTimings") > -1) { + this.owner.lastFinishedRequestCallback(networkInfo, this); + } + }, + + /** + * Update an output message to reflect the latest state of a network request, + * given a network event actor ID. + * + * @private + * @param string actorId + * 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 (actorId) { + let networkInfo = this.webConsoleClient.getNetworkRequest(actorId); + if (!networkInfo || !networkInfo.node) { + return false; + } + + let messageNode = networkInfo.node; + let updates = networkInfo.updates; + 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 = []; + if (response.httpVersion && response.status) { + status = [response.httpVersion, response.status, response.statusText]; + } + if (hasEventTimings) { + status.push(l10n.getFormatStr("NetworkPanel.durationMS", + [networkInfo.totalTime])); + } + let statusText = "[" + status.join(" ") + "]"; + + let statusNode = messageNode.getElementsByClassName("status")[0]; + statusNode.textContent = 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; + }, + + /** + * Opens the network monitor and highlights the specified request. + * + * @param string requestId + * The actor ID of the network request. + */ + openNetworkPanel: function (requestId) { + let toolbox = gDevTools.getToolbox(this.owner.target); + // The browser console doesn't have a toolbox. + if (!toolbox) { + return; + } + return toolbox.selectTool("netmonitor").then(panel => { + return panel.panelWin.NetMonitorController.inspectRequest(requestId); + }); + }, + + /** + * Handler for page location changes. + * + * @param string uri + * New page location. + * @param string title + * New page title. + */ + onLocationChange: function (uri, title) { + this.contentLocation = uri; + if (this.owner.onLocationChange) { + this.owner.onLocationChange(uri, title); + } + }, + + /** + * Handler for the tabNavigated notification. + * + * @param string event + * Event name. + * @param object packet + * Notification packet received from the server. + */ + handleTabNavigated: function (event, packet) { + if (event == "will-navigate") { + if (this.persistLog) { + if (this.NEW_CONSOLE_OUTPUT_ENABLED) { + // Add a _type to hit convertCachedPacket. + packet._type = true; + this.newConsoleOutput.dispatchMessageAdd(packet); + } else { + let marker = new Messages.NavigationMarker(packet, Date.now()); + this.output.addMessage(marker); + } + } else { + this.jsterm.clearOutput(); + } + } + + if (packet.url) { + this.onLocationChange(packet.url, packet.title); + } + + if (event == "navigate" && !packet.nativeConsoleAPI) { + this.logWarningAboutReplacedAPI(); + } + }, + + /** + * Output a message node. This filters a node appropriately, then sends it to + * the output, regrouping and pruning output as necessary. + * + * Note: this call is async - the given message node may not be displayed when + * you call this method. + * + * @param integer category + * The category of the message you want to output. See the CATEGORY_* + * constants. + * @param function|nsIDOMElement methodOrNode + * The method that creates the message element to send to the output or + * the actual element. If a method is given it will be bound to the HUD + * object and the arguments will be |args|. + * @param array [args] + * If a method is given to output the message element then the method + * 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 (category, methodOrNode, args) { + if (!this._outputQueue.length) { + // If the queue is empty we consider that now was the last output flush. + // This avoid an immediate output flush when the timer executes. + this._lastOutputFlush = Date.now(); + } + + this._outputQueue.push([category, methodOrNode, args]); + + this._initOutputTimer(); + }, + + /** + * Try to flush the output message queue. This takes the messages in the + * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL. + * Further output is queued to happen later - see OUTPUT_INTERVAL. + * + * @private + */ + _flushMessageQueue: function () { + this._outputTimerInitialized = false; + if (!this._outputTimer) { + 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 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; + } + + // Try to prune the message queue. + let shouldPrune = false; + if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) { + toDisplay = Math.min(this._outputQueue.length, toDisplay); + shouldPrune = true; + } + + let batch = this._outputQueue.splice(0, toDisplay); + let outputNode = this.outputNode; + let lastVisibleNode = null; + let scrollNode = this.outputWrapper; + 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, scrollNode); + + // Output the current batch of messages. + let messages = new Set(); + for (let i = 0; i < batch.length; i++) { + let item = batch[i]; + let result = this._outputMessageFromQueue(hudIdSupportsString, item); + if (result) { + 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; + } + } + } + + let oldScrollHeight = 0; + let removedNodes = 0; + + // Prune messages from the DOM, but only if needed. + if (shouldPrune || !this._outputQueue.length) { + // 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(category) { + removedNodes += this.pruneOutputIfNecessary(category); + }, this); + this._pruneCategoriesQueue = {}; + } + + let isInputOutput = lastVisibleNode && + (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 + // message. + if (lastVisibleNode && (scrolledToBottom || isInputOutput)) { + Utils.scrollToVisible(lastVisibleNode); + } else if (!scrolledToBottom && removedNodes > 0 && + oldScrollHeight != scrollNode.scrollHeight) { + // If there were pruned messages and if scroll is not at the bottom, then + // we need to adjust the scroll location. + scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight; + } + + if (messages.size) { + this.emit("new-messages", messages); + } + + // 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(); + + // Resize the output area in case a vertical scrollbar has been added + this.resize(); + + this._lastOutputFlush = Date.now(); + }, + + /** + * Initialize the output timer. + * @private + */ + _initOutputTimer: function () { + 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; + } + + this._outputTimerInitialized = true; + this._outputTimer.initWithCallback(this._flushMessageQueue, + OUTPUT_INTERVAL, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Output a message from the queue. + * + * @private + * @param nsISupportsString hudIdSupportsString + * The HUD ID as an nsISupportsString. + * @param array item + * An item from the output queue - this item represents a message. + * @return object + * An object that holds the following properties: + * - node: the DOM element of the message. + * - isRepeated: the DOM element of the original message, if this is + * a repeated message, otherwise null. + * - visible: boolean that tells if the message is visible. + */ + _outputMessageFromQueue: function (hudIdSupportsString, item) { + let [, methodOrNode, args] = item; + + // 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; + if (!node) { + return null; + } + + let isFiltered = this.filterMessageNode(node); + + let isRepeated = this._filterRepeatedMessage(node); + + // If a clear message is processed while the webconsole is opened, the UI + // should be cleared. + // Do not clear the output if the current frame is owned by a Browser Console. + if (message && message.level == "clear" && !this.isBrowserConsole) { + // Do not clear the consoleStorage here as it has been cleared already + // by the clear method, only clear the UI. + this.jsterm.clearOutput(false); + } + + let visible = !isRepeated && !isFiltered; + if (!isRepeated) { + this.outputNode.appendChild(node); + this._pruneCategoriesQueue[node.category] = true; + + let nodeID = node.getAttribute("id"); + Services.obs.notifyObservers(hudIdSupportsString, + "web-console-message-created", nodeID); + } + + if (node._onOutput) { + node._onOutput(); + delete node._onOutput; + } + + return { + visible: visible, + node: node, + isRepeated: isRepeated, + message: message + }; + }, + + /** + * Prune the queue of messages to display. This avoids displaying messages + * that will be removed at the end of the queue anyway. + * @private + */ + _pruneOutputQueue: function () { + let nodes = {}; + + // Group the messages per category. + this._outputQueue.forEach(function (item, index) { + let [category] = item; + if (!(category in nodes)) { + nodes[category] = []; + } + nodes[category].push(index); + }, this); + + let pruned = 0; + + // Loop through the categories we found and prune if needed. + for (let category in nodes) { + let limit = Utils.logLimitForCategory(category); + let indexes = nodes[category]; + if (indexes.length > limit) { + let n = Math.max(0, indexes.length - limit); + pruned += n; + for (let i = n - 1; i >= 0; i--) { + this._itemDestroyQueue.push(this._outputQueue[indexes[i]]); + this._outputQueue.splice(indexes[i], 1); + } + } + } + + return pruned; + }, + + /** + * Destroy an item that was once in the outputQueue but isn't needed + * after all. + * + * @private + * @param array item + * The item you want to destroy. Does not remove it from the output + * queue. + */ + _destroyItem: function (item) { + // TODO: handle object releasing in a more elegant way once all console + // messages use the new API - bug 778766. + let [category, methodOrNode, args] = item; + if (typeof methodOrNode != "function" && methodOrNode._objectActors) { + for (let actor of methodOrNode._objectActors) { + this._releaseObject(actor); + } + 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].actor; + } else if (typeof methodOrNode != "function") { + connectionId = methodOrNode._connectionId; + } + if (connectionId && + this.webConsoleClient.hasNetworkRequest(connectionId)) { + this.webConsoleClient.removeNetworkRequest(connectionId); + this._releaseObject(connectionId); + } + } else if (category == CATEGORY_WEBDEV && + methodOrNode == this.logConsoleAPIMessage) { + args[0].arguments.forEach((value) => { + if (WebConsoleUtils.isActorGrip(value)) { + this._releaseObject(value.actor); + } + }); + } else if (category == CATEGORY_JS && + methodOrNode == this.reportPageError) { + let pageError = args[1]; + for (let prop of ["errorMessage", "lineText"]) { + let grip = pageError[prop]; + if (WebConsoleUtils.isActorGrip(grip)) { + this._releaseObject(grip.actor); + } + } + } else if (category == CATEGORY_JS && + methodOrNode == this._reportLogMessage) { + if (WebConsoleUtils.isActorGrip(args[0].message)) { + this._releaseObject(args[0].message.actor); + } + } + }, + + /** + * Cleans up a message via a node that may or may not + * have actually been rendered in the DOM. Currently, only + * cleans up React components. + * + * @param nsIDOMNode node + * The message node you want to clean up. + */ + unmountMessage(node) { + // Unmount the Frame component with the message location + let locationNode = node.querySelector(".message-location"); + if (locationNode) { + this.ReactDOM.unmountComponentAtNode(locationNode); + } + + // Unmount the StackTrace component if present in the message + let stacktraceNode = node.querySelector(".stacktrace"); + if (stacktraceNode) { + this.ReactDOM.unmountComponentAtNode(stacktraceNode); + } + }, + + /** + * Ensures that the number of message nodes of type category don't exceed that + * category's line limit by removing old messages as needed. + * + * @param integer category + * The category of message nodes to prune if needed. + * @return number + * The number of removed nodes. + */ + pruneOutputIfNecessary: function (category) { + let logLimit = Utils.logLimitForCategory(category); + let messageNodes = this.outputNode.querySelectorAll(".message[category=" + + CATEGORY_CLASS_FRAGMENTS[category] + "]"); + let n = Math.max(0, messageNodes.length - logLimit); + [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this); + return n; + }, + + /** + * Remove a given message from the output. + * + * @param nsIDOMNode node + * The message node you want to remove. + */ + removeOutputMessage: function (node) { + if (node._messageObject) { + node._messageObject.destroy(); + } + + if (node._objectActors) { + for (let actor of node._objectActors) { + this._releaseObject(actor); + } + node._objectActors.clear(); + } + + if (node.category == CATEGORY_CSS || + node.category == CATEGORY_SECURITY) { + let repeatNode = node.getElementsByClassName("message-repeats")[0]; + if (repeatNode && repeatNode._uid) { + delete this._repeatNodes[repeatNode._uid]; + } + } else if (node._connectionId && + node.category == CATEGORY_NETWORK) { + this.webConsoleClient.removeNetworkRequest(node._connectionId); + this._releaseObject(node._connectionId); + } else if (node.classList.contains("inlined-variables-view")) { + let view = node._variablesView; + if (view) { + view.controller.releaseActors(); + } + node._variablesView = null; + } + + this.unmountMessage(node); + + node.remove(); + }, + + /** + * Given a category and message body, creates a DOM node to represent an + * incoming message. The timestamp is automatically added. + * + * @param number category + * The category of the message: one of the CATEGORY_* constants. + * @param number severity + * The severity of the message: one of the SEVERITY_* constants; + * @param string|nsIDOMNode body + * The body of the message, either a simple string or a DOM node. + * @param string sourceURL [optional] + * The URL of the source file that emitted the error. + * @param number sourceLine [optional] + * The line number on which the error occurred. If zero or omitted, + * there is no line number associated with this message. + * @param string clipboardText [optional] + * The text that should be copied to the clipboard when this node is + * copied. If omitted, defaults to the body text. If `body` is not + * a string, then the clipboard text must be supplied. + * @param number level [optional] + * The level of the console API message. + * @param number timestamp [optional] + * The timestamp to use for this message node. If omitted, the current + * date and time is used. + * @return nsIDOMNode + * The message node: a DIV ready to be inserted into the Web Console + * output node. + */ + createMessageNode: function (category, severity, body, sourceURL, sourceLine, + clipboardText, level, timestamp) { + if (typeof body != "string" && clipboardText == null && body.innerText) { + clipboardText = body.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(XHTML_NS, "span"); + iconContainer.className = "icon"; + + // Create the message body, which contains the actual text of the message. + 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 storedBody = body; + // If a string was supplied for the body, turn it into a DOM node and an + // associated clipboard string now. + clipboardText = clipboardText || + (body + (sourceURL ? " @ " + sourceURL : "") + + (sourceLine ? ":" + sourceLine : "")); + + timestamp = timestamp || Date.now(); + + // Create the containing node and append all its elements to it. + let node = this.document.createElementNS(XHTML_NS, "div"); + node.id = "console-msg-" + gSequenceId(); + node.className = "message"; + node.clipboardText = clipboardText; + node.timestamp = timestamp; + this.setMessageType(node, category, severity); + + if (body instanceof Ci.nsIDOMNode) { + bodyNode.appendChild(body); + } else { + let str = undefined; + if (level == "dir") { + str = VariablesView.getString(body.arguments[0]); + } else { + str = body; + } + + if (str !== undefined) { + body = this.document.createTextNode(str); + bodyNode.appendChild(body); + } + } + + // Add the message repeats node only when needed. + let repeatNode = null; + if (category != CATEGORY_INPUT && + category != CATEGORY_OUTPUT && + category != CATEGORY_NETWORK && + !(category == CATEGORY_CSS && severity == SEVERITY_LOG)) { + repeatNode = this.document.createElementNS(XHTML_NS, "span"); + repeatNode.setAttribute("value", "1"); + repeatNode.className = "message-repeats"; + repeatNode.textContent = 1; + repeatNode._uid = [bodyNode.textContent, category, severity, level, + sourceURL, sourceLine].join(":"); + } + + // Create the timestamp. + let timestampNode = this.document.createElementNS(XHTML_NS, "span"); + timestampNode.className = "timestamp devtools-monospace"; + + let timestampString = l10n.timestampString(timestamp); + 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 (sourceURL && IGNORED_SOURCE_URLS.indexOf(sourceURL) == -1) { + locationNode = this.createLocationNode({url: sourceURL, + line: sourceLine}); + } + + node.appendChild(timestampNode); + node.appendChild(indentNode); + node.appendChild(iconContainer); + + // Display the variables view after the message node. + if (level == "dir") { + let options = { + objectActor: storedBody.arguments[0], + targetElement: bodyNode, + hideFilterInput: true, + }; + this.jsterm.openVariablesView(options).then((view) => { + node._variablesView = view; + if (node.classList.contains("hidden-message")) { + node.classList.remove("hidden-message"); + } + }); + + node.classList.add("inlined-variables-view"); + } + + node.appendChild(bodyNode); + if (repeatNode) { + node.appendChild(repeatNode); + } + if (locationNode) { + node.appendChild(locationNode); + } + node.appendChild(this.document.createTextNode("\n")); + + return node; + }, + + /** + * Creates the anchor that displays the textual location of an incoming + * message. + * + * @param {Object} location + * An object containing url, line and column number of the message source. + * @return {Element} + * The new anchor element, ready to be added to the message node. + */ + createLocationNode: function (location) { + let locationNode = this.document.createElementNS(XHTML_NS, "div"); + locationNode.className = "message-location devtools-monospace"; + + // Make the location clickable. + let onClick = ({ url, line }) => { + let category = locationNode.closest(".message").category; + let target = null; + + if (/^Scratchpad\/\d+$/.test(url)) { + target = "scratchpad"; + } else if (category === CATEGORY_CSS) { + target = "styleeditor"; + } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) { + target = "jsdebugger"; + } else if (/\.js$/.test(url)) { + // If it ends in .js, let's attempt to open in debugger + // anyway, as this falls back to normal view-source. + target = "jsdebugger"; + } else { + // Point everything else to debugger, if source not available, + // it will fall back to view-source. + target = "jsdebugger"; + } + + switch (target) { + case "scratchpad": + this.owner.viewSourceInScratchpad(url, line); + return; + case "jsdebugger": + this.owner.viewSourceInDebugger(url, line); + return; + case "styleeditor": + this.owner.viewSourceInStyleEditor(url, line); + return; + } + // No matching tool found; use old school view-source + this.owner.viewSource(url, line); + }; + + const toolbox = gDevTools.getToolbox(this.owner.target); + + let { url, line, column } = location; + let source = url ? url.split(" -> ").pop() : ""; + + this.ReactDOM.render(this.FrameView({ + frame: { source, line, column }, + showEmptyPathAsHost: true, + onClick, + sourceMapService: toolbox ? toolbox._sourceMapService : null, + }), locationNode); + + return locationNode; + }, + + /** + * Adjusts the category and severity of the given message. + * + * @param nsIDOMNode messageNode + * The message node to alter. + * @param number category + * The category for the message; one of the CATEGORY_ constants. + * @param number severity + * The severity for the message; one of the SEVERITY_ constants. + * @return void + */ + setMessageType: function (messageNode, category, severity) { + messageNode.category = category; + messageNode.severity = severity; + messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]); + messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]); + messageNode.setAttribute("filter", + MESSAGE_PREFERENCE_KEYS[category][severity]); + }, + + /** + * Add the mouse event handlers needed to make a link. + * + * @private + * @param nsIDOMNode node + * The node for which you want to add the event handlers. + * @param function callback + * The function you want to invoke on click. + */ + _addMessageLinkCallback: function (node, callback) { + node.addEventListener("mousedown", (event) => { + this._mousedown = true; + this._startX = event.clientX; + this._startY = event.clientY; + }, false); + + node.addEventListener("click", (event) => { + let mousedown = this._mousedown; + this._mousedown = false; + + event.preventDefault(); + + // Do not allow middle/right-click or 2+ clicks. + if (event.detail != 1 || event.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 != event.clientX) && + (this._startY != event.clientY)) { + this._startX = this._startY = undefined; + return; + } + + this._startX = this._startY = undefined; + + callback.call(this, event); + }, false); + }, + + /** + * Handler for the pref-changed event coming from the toolbox. + * Currently this function only handles the timestamps preferences. + * + * @private + * @param object event + * This parameter is a string that holds the event name + * pref-changed in this case. + * @param object data + * This is the pref-changed data object. + */ + _onToolboxPrefChanged: function (event, data) { + if (data.pref == PREF_MESSAGE_TIMESTAMP) { + if (data.newValue) { + this.outputNode.classList.remove("hideTimestamps"); + } else { + this.outputNode.classList.add("hideTimestamps"); + } + } + }, + + /** + * Copies the selected items to the system clipboard. + * + * @param object options + * - linkOnly: + * An optional flag to copy only URL without 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 (options) { + options = options || { linkOnly: false, contextmenu: false }; + + // Gather up the selected items and concatenate their clipboard text. + let strings = []; + + let children = this.output.getSelectedMessages(); + if (!children.length && options.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("filtered-by-type") && + !item.classList.contains("filtered-by-string")) { + if (options.linkOnly) { + strings.push(item.url); + } else { + strings.push(item.clipboardText); + } + } + } + + clipboardHelper.copyString(strings.join("\n")); + }, + + /** + * Object properties provider. This function gives you the properties of the + * remote object you want. + * + * @param string actor + * The object actor ID from which you want the properties. + * @param function callback + * Function you want invoked once the properties are received. + */ + objectPropertiesProvider: function (actor, callback) { + this.webConsoleClient.inspectObjectProperties(actor, + function (response) { + if (response.error) { + console.error("Failed to retrieve the object properties from the " + + "server. Error: " + response.error); + return; + } + callback(response.properties); + }); + }, + + /** + * Release an actor. + * + * @private + * @param string actor + * The actor ID you want to release. + */ + _releaseObject: function (actor) { + if (this.proxy) { + this.proxy.releaseActor(actor); + } + }, + + /** + * Open the selected item's URL in a new tab. + */ + openSelectedItemInTab: function () { + let item = this.output.getSelectedMessages(1)[0] || + this._contextMenuHandler.lastClickedMessage; + + if (!item || !item.url) { + return; + } + + this.owner.openLink(item.url); + }, + + /** + * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks + * when the Web Console is closed. + * + * @return object + * A promise that is resolved when the WebConsoleFrame instance is + * destroyed. + */ + destroy: function () { + if (this._destroyer) { + return this._destroyer.promise; + } + + 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.window.removeEventListener("resize", this.resize, true); + + this._repeatNodes = {}; + this._outputQueue.forEach(this._destroyItem, this); + this._outputQueue = []; + this._itemDestroyQueue.forEach(this._destroyItem, this); + this._itemDestroyQueue = []; + this._pruneCategoriesQueue = {}; + this.webConsoleClient.clearNetworkRequests(); + + // Unmount any currently living frame components in DOM, since + // currently we only clean up messages in `this.removeOutputMessage`, + // via `this.pruneOutputIfNecessary`. + let liveMessages = this.outputNode.querySelectorAll(".message"); + Array.prototype.forEach.call(liveMessages, this.unmountMessage); + + if (this._outputTimerInitialized) { + this._outputTimerInitialized = false; + this._outputTimer.cancel(); + } + this._outputTimer = null; + if (this.jsterm) { + this.jsterm.off("sidebar-opened", this.resize); + this.jsterm.off("sidebar-closed", this.resize); + this.jsterm.destroy(); + this.jsterm = null; + } + this.output.destroy(); + this.output = null; + + this.React = this.ReactDOM = this.FrameView = null; + + if (this._contextMenuHandler) { + this._contextMenuHandler.destroy(); + this._contextMenuHandler = null; + } + + this._commandController = null; + + let onDestroy = () => { + this._destroyer.resolve(null); + }; + + if (this.proxy) { + this.proxy.disconnect().then(onDestroy); + this.proxy = null; + } else { + onDestroy(); + } + + return this._destroyer.promise; + }, +}; + +/** + * Utils: a collection of globally used functions. + */ +var Utils = { + /** + * Scrolls a node so that it's visible in its containing element. + * + * @param nsIDOMNode node + * The node to make visible. + * @returns void + */ + scrollToVisible: function (node) { + node.scrollIntoView(false); + }, + + /** + * Check if the given output node is scrolled to the bottom. + * + * @param nsIDOMNode outputNode + * @param nsIDOMNode scrollNode + * @return boolean + * True if the output node is scrolled to the bottom, or false + * otherwise. + */ + isOutputScrolledToBottom: function (outputNode, scrollNode) { + let lastNodeHeight = outputNode.lastChild ? + outputNode.lastChild.clientHeight : 0; + return scrollNode.scrollTop + scrollNode.clientHeight >= + scrollNode.scrollHeight - lastNodeHeight / 2; + }, + + /** + * Determine the category of a given nsIScriptError. + * + * @param nsIScriptError scriptError + * The script error you want to determine the category for. + * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY + * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or + * CATEGORY_SECURITY can be returned. + */ + categoryForScriptError: function (scriptError) { + let category = scriptError.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": + case "Sub-resource Integrity": + return CATEGORY_SECURITY; + + default: + return CATEGORY_JS; + } + }, + + /** + * Retrieve the limit of messages for a specific category. + * + * @param number category + * The category of messages you want to retrieve the limit for. See the + * CATEGORY_* constants. + * @return number + * The number of messages allowed for the specific category. + */ + logLimitForCategory: function (category) { + let logLimit = DEFAULT_LOG_LIMIT; + + try { + let prefName = CATEGORY_CLASS_FRAGMENTS[category]; + logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); + logLimit = Math.max(logLimit, 1); + } catch (e) { + // Ignore any exceptions + } + + return logLimit; + }, +}; + +// CommandController + +/** + * A controller (an instance of nsIController) that makes editing actions + * behave appropriately in the context of the Web Console. + */ +function CommandController(webConsole) { + this.owner = webConsole; +} + +CommandController.prototype = { + /** + * Selects all the text in the HUD output. + */ + selectAll: function () { + this.owner.output.selectAllMessages(); + }, + + /** + * Open the URL of the selected message in a new tab. + */ + openURL: function () { + this.owner.openSelectedItemInTab(); + }, + + copyURL: function () { + this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); + }, + + /** + * Copies the last clicked message. + */ + copyLastClicked: function () { + this.owner.copySelectedItems({ linkOnly: false, contextmenu: true }); + }, + + supportsCommand: function (command) { + if (!this.owner || !this.owner.output) { + return false; + } + return this.isCommandEnabled(command); + }, + + isCommandEnabled: function (command) { + switch (command) { + case "consoleCmd_openURL": + case "consoleCmd_copyURL": { + // Only enable URL-related actions if node is Net Activity. + 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 "cmd_selectAll": + return true; + } + return false; + }, + + doCommand: function (command) { + switch (command) { + case "consoleCmd_openURL": + this.openURL(); + break; + case "consoleCmd_copyURL": + this.copyURL(); + break; + case "cmd_copy": + this.copyLastClicked(); + break; + case "cmd_selectAll": + this.selectAll(); + break; + } + } +}; + +// Web Console connection proxy + +/** + * The WebConsoleConnectionProxy handles the connection between the Web Console + * and the application we connect to through the remote debug protocol. + * + * @constructor + * @param object webConsoleFrame + * The WebConsoleFrame object that owns this connection proxy. + * @param RemoteTarget target + * The target that the console will connect to. + */ +function WebConsoleConnectionProxy(webConsoleFrame, target) { + this.webConsoleFrame = webConsoleFrame; + this.target = target; + + this._onPageError = this._onPageError.bind(this); + this._onLogMessage = this._onLogMessage.bind(this); + this._onConsoleAPICall = this._onConsoleAPICall.bind(this); + 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._onServerLogCall = this._onServerLogCall.bind(this); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onAttachConsole = this._onAttachConsole.bind(this); + this._onCachedMessages = this._onCachedMessages.bind(this); + this._connectionTimeout = this._connectionTimeout.bind(this); + this._onLastPrivateContextExited = + this._onLastPrivateContextExited.bind(this); +} + +WebConsoleConnectionProxy.prototype = { + /** + * The owning Web Console Frame instance. + * + * @see WebConsoleFrame + * @type object + */ + webConsoleFrame: null, + + /** + * The target that the console connects to. + * @type RemoteTarget + */ + target: null, + + /** + * The DebuggerClient object. + * + * @see DebuggerClient + * @type object + */ + client: null, + + /** + * The WebConsoleClient object. + * + * @see WebConsoleClient + * @type object + */ + webConsoleClient: null, + + /** + * Tells if the connection is established. + * @type boolean + */ + connected: false, + + /** + * Timer used for the connection. + * @private + * @type object + */ + _connectTimer: null, + + _connectDefer: null, + _disconnecter: null, + + /** + * The WebConsoleActor ID. + * + * @private + * @type string + */ + _consoleActor: null, + + /** + * Tells if the window.console object of the remote web page is the native + * object or not. + * @private + * @type boolean + */ + _hasNativeConsoleAPI: false, + + /** + * Initialize a debugger client and connect it to the debugger server. + * + * @return object + * A promise object that is resolved/rejected based on the success of + * the connection initialization. + */ + connect: function () { + if (this._connectDefer) { + return this._connectDefer.promise; + } + + this._connectDefer = promise.defer(); + + let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); + this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._connectTimer.initWithCallback(this._connectionTimeout, + timeout, Ci.nsITimer.TYPE_ONE_SHOT); + + let connPromise = this._connectDefer.promise; + connPromise.then(() => { + this._connectTimer.cancel(); + this._connectTimer = null; + }, () => { + this._connectTimer = null; + }); + + let client = this.client = this.target.client; + + if (this.target.isWorkerTarget) { + // XXXworkers: Not Console API yet inside of workers (Bug 1209353). + } else { + client.addListener("logMessage", this._onLogMessage); + client.addListener("pageError", this._onPageError); + client.addListener("consoleAPICall", this._onConsoleAPICall); + client.addListener("fileActivity", this._onFileActivity); + client.addListener("reflowActivity", this._onReflowActivity); + client.addListener("serverLogCall", this._onServerLogCall); + client.addListener("lastPrivateContextExited", + this._onLastPrivateContextExited); + } + this.target.on("will-navigate", this._onTabNavigated); + this.target.on("navigate", this._onTabNavigated); + + this._consoleActor = this.target.form.consoleActor; + if (this.target.isTabActor) { + let tab = this.target.form; + this.webConsoleFrame.onLocationChange(tab.url, tab.title); + } + this._attachConsole(); + + return connPromise; + }, + + /** + * Connection timeout handler. + * @private + */ + _connectionTimeout: function () { + let error = { + error: "timeout", + message: l10n.getStr("connectionTimeout"), + }; + + this._connectDefer.reject(error); + }, + + /** + * Attach to the Web Console actor. + * @private + */ + _attachConsole: function () { + let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", + "FileActivity"]; + this.client.attachConsole(this._consoleActor, listeners, + this._onAttachConsole); + }, + + /** + * The "attachConsole" response handler. + * + * @private + * @param object response + * The JSON response object received from the server. + * @param object webConsoleClient + * The WebConsoleClient instance for the attached console, for the + * specific tab we work with. + */ + _onAttachConsole: function (response, webConsoleClient) { + if (response.error) { + console.error("attachConsole failed: " + response.error + " " + + response.message); + this._connectDefer.reject(response); + return; + } + + this.webConsoleClient = webConsoleClient; + this._hasNativeConsoleAPI = response.nativeConsoleAPI; + + // There is no way to view response bodies from the Browser Console, so do + // not waste the memory. + let saveBodies = !this.webConsoleFrame.isBrowserConsole; + this.webConsoleFrame.setSaveRequestAndResponseBodies(saveBodies); + + this.webConsoleClient.on("networkEvent", this._onNetworkEvent); + this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate); + + let msgs = ["PageError", "ConsoleAPI"]; + this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); + + this.webConsoleFrame._onUpdateListeners(); + }, + + /** + * Dispatch a message add on the new frontend and emit an event for tests. + */ + dispatchMessageAdd: function(packet) { + this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet); + }, + + /** + * Batched dispatch of messages. + */ + dispatchMessagesAdd: function(packets) { + this.webConsoleFrame.newConsoleOutput.dispatchMessagesAdd(packets); + }, + + /** + * The "cachedMessages" response handler. + * + * @private + * @param object response + * The JSON response object received from the server. + */ + _onCachedMessages: function (response) { + if (response.error) { + console.error("Web Console getCachedMessages error: " + response.error + + " " + response.message); + this._connectDefer.reject(response); + return; + } + + if (!this._connectTimer) { + // This happens if the promise is rejected (eg. a timeout), but the + // connection attempt is successful, nonetheless. + console.error("Web Console getCachedMessages error: invalid state."); + } + + let messages = + response.messages.concat(...this.webConsoleClient.getNetworkEvents()); + messages.sort((a, b) => a.timeStamp - b.timeStamp); + + if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) { + // Filter out CSS page errors. + messages = messages.filter(message => !(message._type == "PageError" + && Utils.categoryForScriptError(message) === CATEGORY_CSS)); + this.dispatchMessagesAdd(messages); + } else { + this.webConsoleFrame.displayCachedMessages(messages); + if (!this._hasNativeConsoleAPI) { + this.webConsoleFrame.logWarningAboutReplacedAPI(); + } + } + + this.connected = true; + this._connectDefer.resolve(this); + }, + + /** + * The "pageError" message type handler. We redirect any page errors to the UI + * for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onPageError: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) { + let category = Utils.categoryForScriptError(packet.pageError); + if (category !== CATEGORY_CSS) { + this.dispatchMessageAdd(packet); + } + return; + } + this.webConsoleFrame.handlePageError(packet.pageError); + } + }, + + /** + * The "logMessage" message type handler. We redirect any message to the UI + * for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onLogMessage: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + this.webConsoleFrame.handleLogMessage(packet); + } + }, + + /** + * The "consoleAPICall" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onConsoleAPICall: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) { + this.dispatchMessageAdd(packet); + } else { + this.webConsoleFrame.handleConsoleAPICall(packet.message); + } + } + }, + + /** + * The "networkEvent" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object networkInfo + * The network request information. + */ + _onNetworkEvent: function (type, networkInfo) { + if (this.webConsoleFrame) { + if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) { + this.dispatchMessageAdd(networkInfo); + } else { + this.webConsoleFrame.handleNetworkEvent(networkInfo); + } + } + }, + + /** + * The "networkEventUpdate" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + * @param object networkInfo + * The network request information. + */ + _onNetworkEventUpdate: function (type, { packet, networkInfo }) { + if (this.webConsoleFrame) { + this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet); + } + }, + + /** + * The "fileActivity" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onFileActivity: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + this.webConsoleFrame.handleFileActivity(packet.uri); + } + }, + + _onReflowActivity: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + this.webConsoleFrame.handleReflowActivity(packet); + } + }, + + /** + * The "serverLogCall" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onServerLogCall: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + this.webConsoleFrame.handleConsoleAPICall(packet.message); + } + }, + + /** + * The "lastPrivateContextExited" message type handler. When this message is + * received the Web Console UI is cleared. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onLastPrivateContextExited: function (type, packet) { + if (this.webConsoleFrame && packet.from == this._consoleActor) { + this.webConsoleFrame.jsterm.clearPrivateMessages(); + } + }, + + /** + * The "will-navigate" and "navigate" event handlers. We redirect any message + * to the UI for displaying. + * + * @private + * @param string event + * Event type. + * @param object packet + * The message received from the server. + */ + _onTabNavigated: function (event, packet) { + if (!this.webConsoleFrame) { + return; + } + + this.webConsoleFrame.handleTabNavigated(event, packet); + }, + + /** + * Release an object actor. + * + * @param string actor + * The actor ID to send the request to. + */ + releaseActor: function (actor) { + if (this.client) { + this.client.release(actor); + } + }, + + /** + * Disconnect the Web Console from the remote server. + * + * @return object + * A promise object that is resolved when disconnect completes. + */ + disconnect: function () { + if (this._disconnecter) { + return this._disconnecter.promise; + } + + this._disconnecter = promise.defer(); + + if (!this.client) { + this._disconnecter.resolve(null); + return this._disconnecter.promise; + } + + this.client.removeListener("logMessage", this._onLogMessage); + this.client.removeListener("pageError", this._onPageError); + this.client.removeListener("consoleAPICall", this._onConsoleAPICall); + this.client.removeListener("fileActivity", this._onFileActivity); + this.client.removeListener("reflowActivity", this._onReflowActivity); + this.client.removeListener("serverLogCall", this._onServerLogCall); + this.client.removeListener("lastPrivateContextExited", + this._onLastPrivateContextExited); + this.webConsoleClient.off("networkEvent", this._onNetworkEvent); + this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate); + this.target.off("will-navigate", this._onTabNavigated); + this.target.off("navigate", this._onTabNavigated); + + this.client = null; + this.webConsoleClient = null; + this.target = null; + this.connected = false; + this.webConsoleFrame = null; + this._disconnecter.resolve(null); + + return this._disconnecter.promise; + }, +}; + +// Context Menu + +/* + * ConsoleContextMenu this used to handle the visibility of context menu items. + * + * @constructor + * @param object owner + * The WebConsoleFrame instance that owns this object. + */ +function ConsoleContextMenu(owner) { + this.owner = owner; + 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. + */ + build: function (event) { + let metadata = this.getSelectionMetadata(event.rangeParent); + for (let element of this.popup.children) { + element.hidden = this.shouldHideMenuItem(element, metadata); + } + }, + + /* + * Get selection information from the view. + * + * @param nsIDOMElement clickElement + * The DOM element the user clicked on. + * @return object + * Selection metadata. + */ + getSelectionMetadata: function (clickElement) { + let metadata = { + selectionType: "", + selection: new Set(), + }; + let selectedItems = this.owner.output.getSelectedMessages(); + if (!selectedItems.length) { + let clickedItem = this.owner.output.getMessageForElement(clickElement); + if (clickedItem) { + this.lastClickedMessage = clickedItem; + selectedItems = [clickedItem]; + } + } + + metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; + + let selection = metadata.selection; + for (let item of selectedItems) { + switch (item.category) { + case CATEGORY_NETWORK: + selection.add("network"); + break; + case CATEGORY_CSS: + selection.add("css"); + break; + case CATEGORY_JS: + selection.add("js"); + break; + case CATEGORY_WEBDEV: + selection.add("webdev"); + break; + case CATEGORY_SERVER: + selection.add("server"); + break; + } + } + + return metadata; + }, + + /* + * Determine if an item should be hidden. + * + * @param nsIDOMElement menuItem + * @param object metadata + * @return boolean + * Whether the given item should be hidden or not. + */ + shouldHideMenuItem: function (menuItem, metadata) { + let selectionType = menuItem.getAttribute("selectiontype"); + if (selectionType && !metadata.selectionType == selectionType) { + return true; + } + + let selection = menuItem.getAttribute("selection"); + if (!selection) { + return false; + } + + let shouldHide = true; + let itemData = selection.split("|"); + for (let type of metadata.selection) { + // check whether this menu item should show or not. + if (itemData.indexOf(type) !== -1) { + shouldHide = false; + break; + } + } + + return shouldHide; + }, + + /** + * Destroy the ConsoleContextMenu object instance. + */ + destroy: function () { + this.popup.removeEventListener("popupshowing", this.build); + this.popup = null; + this.owner = null; + this.lastClickedMessage = null; + }, +}; diff --git a/devtools/client/webconsole/webconsole.xul b/devtools/client/webconsole/webconsole.xul new file mode 100644 index 0000000000..cd3e44d820 --- /dev/null +++ b/devtools/client/webconsole/webconsole.xul @@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<!DOCTYPE window [ +<!ENTITY % webConsoleDTD SYSTEM "chrome://devtools/locale/webConsole.dtd"> +%webConsoleDTD; +]> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/widgets.css" + type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/webconsole.css" + type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/components-frame.css" + type="text/css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="devtools-webconsole" + macanimationtype="document" + fullscreenbutton="true" + title="&window.title;" + browserConsoleTitle="&browserConsole.title;" + windowtype="devtools:webconsole" + width="900" height="350" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript;version=1.8" + src="chrome://devtools/content/shared/theme-switching.js"/> + <script type="application/javascript;version=1.8" + src="resource://devtools/client/webconsole/new-console-output/main.js"/> + <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/> + <script type="text/javascript"><![CDATA[ +function goUpdateConsoleCommands() { + goUpdateCommand("consoleCmd_openURL"); + goUpdateCommand("consoleCmd_copyURL"); +} + // ]]></script> + + <commandset id="editMenuCommands"/> + + <commandset id="consoleCommands" + commandupdater="true" + events="focus,select" + oncommandupdate="goUpdateConsoleCommands();"> + <command id="consoleCmd_openURL" + oncommand="goDoCommand('consoleCmd_openURL');"/> + <command id="consoleCmd_copyURL" + oncommand="goDoCommand('consoleCmd_copyURL');"/> + </commandset> + <keyset id="consoleKeys"> + </keyset> + <keyset id="editMenuKeys"/> + + <popupset id="mainPopupSet"> + <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()"> + <menuitem id="menu_openURL" label="&openURL.label;" + accesskey="&openURL.accesskey;" command="consoleCmd_openURL" + selection="network" selectionType="single"/> + <menuitem id="menu_copyURL" label="©URLCmd.label;" + accesskey="©URLCmd.accesskey;" command="consoleCmd_copyURL" + selection="network" selectionType="single"/> + <menuitem id="menu_openInVarView" label="&openInVarViewCmd.label;" + accesskey="&openInVarViewCmd.accesskey;" disabled="true"/> + <menuitem id="menu_storeAsGlobal" label="&storeAsGlobalVar.label;" + accesskey="&storeAsGlobalVar.accesskey;"/> + <menuitem id="cMenu_copy"/> + <menuitem id="cMenu_selectAll"/> + </menupopup> + </popupset> + + <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"> + <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton devtools-clear-icon" + tooltiptext="&btnClear.tooltip;" + accesskey="&btnClear.accesskey;" + tabindex="3"/> + <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="4"> + <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"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnPageCSS.label;" type="menu-button" + category="css" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnPageCSS.tooltip2;" + accesskey="&btnPageCSS.accesskey;" + tabindex="5"> + <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="6"> + <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="7"> + <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="8"> + <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"/> + <menuseparator /> + <menuitem label="&btnConsoleSharedWorkers;" type="checkbox" + autocheck="false" prefKey="sharedworkers"/> + <menuitem label="&btnConsoleServiceWorkers;" type="checkbox" + autocheck="false" prefKey="serviceworkers"/> + <menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox" + autocheck="false" prefKey="windowlessworkers"/> + </menupopup> + </toolbarbutton> + <toolbarbutton label="&btnServerLogging.label;" type="menu-button" + category="server" class="devtools-toolbarbutton webconsole-filter-button" + tooltiptext="&btnServerLogging.tooltip;" + accesskey="&btnServerLogging.accesskey;" + tabindex="9"> + <menupopup id="server-logging-contextmenu"> + <menuitem label="&btnServerErrors;" type="checkbox" + autocheck="false" prefKey="servererror"/> + <menuitem label="&btnServerWarnings;" type="checkbox" + autocheck="false" prefKey="serverwarn"/> + <menuitem label="&btnServerInfo;" type="checkbox" autocheck="false" + prefKey="serverinfo"/> + <menuitem label="&btnServerLog;" type="checkbox" autocheck="false" + prefKey="serverlog"/> + </menupopup> + </toolbarbutton> + </hbox> + + <spacer flex="1"/> + + <textbox class="compact hud-filter-box devtools-filterinput" type="search" + placeholder="&filterOutput.placeholder;" tabindex="2"/> + </toolbar> + + <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip"> + <!-- Wrapper element to make scrolling in output-container much faster. + See Bug 1237368 --> + <div xmlns="http://www.w3.org/1999/xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml" id="output-container" + tabindex="0" role="document" aria-live="polite" /> + </div> + </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"/> + + <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300"> + <tabs/> + <tabpanels flex="1"/> + </tabbox> + </box> +</window> |