summaryrefslogtreecommitdiff
path: root/toolkit/devtools/webconsole
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/webconsole')
-rw-r--r--toolkit/devtools/webconsole/NetworkPanel.xhtml124
-rw-r--r--toolkit/devtools/webconsole/console-commands.js88
-rw-r--r--toolkit/devtools/webconsole/console-output.js3567
-rw-r--r--toolkit/devtools/webconsole/hudservice.js789
-rw-r--r--toolkit/devtools/webconsole/moz.build14
-rw-r--r--toolkit/devtools/webconsole/network-panel.js836
-rw-r--r--toolkit/devtools/webconsole/panel.js122
-rw-r--r--toolkit/devtools/webconsole/test/chrome.ini28
-rw-r--r--toolkit/devtools/webconsole/test/common.js204
-rw-r--r--toolkit/devtools/webconsole/test/data.json3
-rw-r--r--toolkit/devtools/webconsole/test/data.json^headers^3
-rw-r--r--toolkit/devtools/webconsole/test/network_requests_iframe.html61
-rw-r--r--toolkit/devtools/webconsole/test/sandboxed_iframe.html8
-rw-r--r--toolkit/devtools/webconsole/test/test_basics.html79
-rw-r--r--toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html76
-rw-r--r--toolkit/devtools/webconsole/test/test_cached_messages.html230
-rw-r--r--toolkit/devtools/webconsole/test/test_consoleapi.html199
-rw-r--r--toolkit/devtools/webconsole/test/test_consoleapi_innerID.html164
-rw-r--r--toolkit/devtools/webconsole/test/test_file_uri.html106
-rw-r--r--toolkit/devtools/webconsole/test/test_jsterm.html319
-rw-r--r--toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html223
-rw-r--r--toolkit/devtools/webconsole/test/test_network_get.html260
-rw-r--r--toolkit/devtools/webconsole/test/test_network_longstring.html305
-rw-r--r--toolkit/devtools/webconsole/test/test_network_post.html286
-rw-r--r--toolkit/devtools/webconsole/test/test_network_security-hpkp.html108
-rw-r--r--toolkit/devtools/webconsole/test/test_network_security-hsts.html100
-rw-r--r--toolkit/devtools/webconsole/test/test_nsiconsolemessage.html74
-rw-r--r--toolkit/devtools/webconsole/test/test_object_actor.html178
-rw-r--r--toolkit/devtools/webconsole/test/test_object_actor_native_getters.html106
-rw-r--r--toolkit/devtools/webconsole/test/test_object_actor_native_getters_lenient_this.html79
-rw-r--r--toolkit/devtools/webconsole/test/test_page_errors.html102
-rw-r--r--toolkit/devtools/webconsole/test/test_reflow.html94
-rw-r--r--toolkit/devtools/webconsole/test/test_throw.html78
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_js_property_provider.js71
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_network_helper.js47
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js68
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-parser.js64
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js54
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-state.js100
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-static-hpkp.js47
-rw-r--r--toolkit/devtools/webconsole/test/unit/test_security-info-weakness-reasons.js55
-rw-r--r--toolkit/devtools/webconsole/test/unit/xpcshell.ini14
-rw-r--r--toolkit/devtools/webconsole/webconsole.js5456
-rw-r--r--toolkit/devtools/webconsole/webconsole.xul202
44 files changed, 11194 insertions, 3997 deletions
diff --git a/toolkit/devtools/webconsole/NetworkPanel.xhtml b/toolkit/devtools/webconsole/NetworkPanel.xhtml
new file mode 100644
index 000000000..62753ae4f
--- /dev/null
+++ b/toolkit/devtools/webconsole/NetworkPanel.xhtml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % webConsoleDTD SYSTEM "chrome://browser/locale/devtools/webConsole.dtd" >
+%webConsoleDTD;
+]>
+
+<!-- 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 xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/webconsole_networkpanel.css" type="text/css"/>
+</head>
+<body role="application">
+<table id="header">
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.requestURLColon;</th>
+ <td class="property-value"
+ id="headUrl"></td>
+ </tr>
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.requestMethodColon;</th>
+ <td class="property-value"
+ id="headMethod"></td>
+ </tr>
+ <tr>
+ <th class="property-name"
+ scope="row">&networkPanel.statusCodeColon;</th>
+ <td class="property-value"
+ id="headStatus"></td>
+ </tr>
+</table>
+
+<div class="group">
+ <h1>
+ &networkPanel.requestHeaders;
+ <span id="requestHeadersInfo" class="info"></span>
+ </h1>
+ <table class="property-table" id="requestHeadersContent"></table>
+
+ <div id="requestCookie" style="display:none">
+ <h1>&networkPanel.requestCookie;</h1>
+ <table class="property-table" id="requestCookieContent"></table>
+ </div>
+
+ <div id="requestBody" style="display:none">
+ <h1>&networkPanel.requestBody;</h1>
+ <table class="property-table" id="requestBodyContent"></table>
+ </div>
+ <div id="requestFormData" style="display:none">
+ <h1>&networkPanel.requestFormData;</h1>
+ <table class="property-table" id="requestFormDataContent"></table>
+ </div>
+ <p id="requestBodyFetchLink" style="display:none"></p>
+</div>
+
+<div class="group" id="responseContainer" style="display:none">
+ <h1>
+ &networkPanel.responseHeaders;
+ <span id="responseHeadersInfo" class="info">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseHeadersContent"></table>
+
+ <div id="responseCookie" style="display:none">
+ <h1>&networkPanel.responseCookie;</h1>
+ <table class="property-table" id="responseCookieContent"></table>
+ </div>
+
+ <div id="responseBody" style="display:none">
+ <h1>
+ &networkPanel.responseBody;
+ <span class="info" id="responseBodyInfo">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyContent"></table>
+ </div>
+ <div id="responseBodyCached" style="display:none">
+ <h1>
+ &networkPanel.responseBodyCached;
+ <span class="info" id="responseBodyCachedInfo">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyCachedContent"></table>
+ </div>
+ <div id="responseNoBody" style="display:none">
+ <h1>
+ &networkPanel.responseNoBody;
+ <span id="responseNoBodyInfo" class="info">&Delta;</span>
+ </h1>
+ </div>
+ <div id="responseBodyUnknownType" style="display:none">
+ <h1>
+ &networkPanel.responseBodyUnknownType;
+ <span id="responseBodyUnknownTypeInfo" class="info">&Delta;</span>
+ </h1>
+ <table class="property-table" id="responseBodyUnknownTypeContent"></table>
+ </div>
+ <div id="responseImage" style="display:none">
+ <h1>
+ &networkPanel.responseImage;
+ <span id="responseImageInfo" class="info"></span>
+ </h1>
+ <div id="responseImageNodeDiv">
+ <img id="responseImageNode" />
+ </div>
+ </div>
+ <div id="responseImageCached" style="display:none">
+ <h1>
+ &networkPanel.responseImageCached;
+ <span id="responseImageCachedInfo" class="info"></span>
+ </h1>
+ <div id="responseImageNodeDiv">
+ <img id="responseImageCachedNode" />
+ </div>
+ </div>
+ <p id="responseBodyFetchLink" style="display:none"></p>
+</div>
+</body>
+</html>
diff --git a/toolkit/devtools/webconsole/console-commands.js b/toolkit/devtools/webconsole/console-commands.js
new file mode 100644
index 000000000..471362c08
--- /dev/null
+++ b/toolkit/devtools/webconsole/console-commands.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const gcli = require("gcli/index");
+const { gDevTools } = require("resource:///modules/devtools/gDevTools.jsm");
+
+exports.items = [
+ {
+ name: 'splitconsole',
+ hidden: true,
+ buttonId: "command-button-splitconsole",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: gcli.lookup("splitconsoleTooltip"),
+ isRemoteSafe: true,
+ state: {
+ isChecked: function(target) {
+ let toolbox = gDevTools.getToolbox(target);
+ return !!(toolbox && toolbox.splitConsole);
+ },
+ onChange: function(target, changeHandler) {
+ // Register handlers for when a change event should be fired
+ // (which resets the checked state of the button).
+ let toolbox = gDevTools.getToolbox(target);
+ let callback = changeHandler.bind(null, "changed", { target: target });
+
+ if (!toolbox) {
+ return;
+ }
+
+ toolbox.on("split-console", callback);
+ toolbox.once("destroyed", () => {
+ toolbox.off("split-console", callback);
+ });
+ }
+ },
+ exec: function(args, context) {
+ let target = context.environment.target;
+ let toolbox = gDevTools.getToolbox(target);
+
+ if (!toolbox) {
+ return gDevTools.showToolbox(target, "inspector").then((toolbox) => {
+ toolbox.toggleSplitConsole();
+ });
+ } else {
+ toolbox.toggleSplitConsole();
+ }
+ }
+ },
+ {
+ name: "console",
+ description: gcli.lookup("consoleDesc"),
+ manual: gcli.lookup("consoleManual")
+ },
+ {
+ name: "console clear",
+ description: gcli.lookup("consoleclearDesc"),
+ exec: function(args, context) {
+ let toolbox = gDevTools.getToolbox(context.environment.target);
+ if (toolbox == null) {
+ return;
+ }
+
+ let panel = toolbox.getPanel("webconsole");
+ if (panel == null) {
+ return;
+ }
+
+ panel.hud.jsterm.clearOutput();
+ }
+ },
+ {
+ name: "console close",
+ description: gcli.lookup("consolecloseDesc"),
+ exec: function(args, context) {
+ return gDevTools.closeToolbox(context.environment.target);
+ }
+ },
+ {
+ name: "console open",
+ description: gcli.lookup("consoleopenDesc"),
+ exec: function(args, context) {
+ return gDevTools.showToolbox(context.environment.target, "webconsole");
+ }
+ }
+];
diff --git a/toolkit/devtools/webconsole/console-output.js b/toolkit/devtools/webconsole/console-output.js
new file mode 100644
index 000000000..f78e24cbd
--- /dev/null
+++ b/toolkit/devtools/webconsole/console-output.js
@@ -0,0 +1,3567 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
+loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm");
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
+loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "TableWidget", "devtools/shared/widgets/TableWidget", true);
+
+const Heritage = require("sdk/core/heritage");
+const URI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+
+const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+const l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+const MAX_STRING_GRIP_LENGTH = 36;
+const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+
+// Constants for compatibility with the Web Console output implementation before
+// bug 778766.
+// TODO: remove these once bug 778766 is fixed.
+const COMPAT = {
+ // The various categories of messages.
+ CATEGORIES: {
+ NETWORK: 0,
+ CSS: 1,
+ JS: 2,
+ WEBDEV: 3,
+ INPUT: 4,
+ OUTPUT: 5,
+ SECURITY: 6,
+ },
+
+ // The possible message severities.
+ SEVERITIES: {
+ ERROR: 0,
+ WARNING: 1,
+ INFO: 2,
+ LOG: 3,
+ },
+
+ // The preference keys to use for each category/severity combination, indexed
+ // first by category (rows) and then by severity (columns).
+ //
+ // Most of these rather idiosyncratic names are historical and predate the
+ // division of message type into "category" and "severity".
+ PREFERENCE_KEYS: [
+ // Error Warning Info Log
+ [ "network", "netwarn", null, "networkinfo", ], // Network
+ [ "csserror", "cssparser", null, null, ], // CSS
+ [ "exception", "jswarn", null, "jslog", ], // JS
+ [ "error", "warn", "info", "log", ], // Web Developer
+ [ null, null, null, null, ], // Input
+ [ null, null, null, null, ], // Output
+ [ "secerror", "secwarn", null, null, ], // Security
+ ],
+
+ // The fragment of a CSS class name that identifies each category.
+ CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
+ "input", "output", "security" ],
+
+ // The fragment of a CSS class name that identifies each severity.
+ SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
+
+ // The indent of a console group in pixels.
+ GROUP_INDENT: 12,
+};
+
+// A map from the console API call levels to the Web Console severities.
+const CONSOLE_API_LEVELS_TO_SEVERITIES = {
+ error: "error",
+ exception: "error",
+ assert: "error",
+ warn: "warning",
+ info: "info",
+ log: "log",
+ trace: "log",
+ table: "log",
+ debug: "log",
+ dir: "log",
+ group: "log",
+ groupCollapsed: "log",
+ groupEnd: "log",
+ time: "log",
+ timeEnd: "log",
+ count: "log"
+};
+
+// Array of known message source URLs we need to hide from output.
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+// The maximum length of strings to be displayed by the Web Console.
+const MAX_LONG_STRING_LENGTH = 200000;
+
+// Regular expression that matches the allowed CSS property names when using
+// the `window.console` API.
+const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/;
+
+// Regular expressions to search and replace with 'notallowed' in the styles
+// given to the `window.console` API methods.
+const RE_CLEANUP_STYLES = [
+ // url(), -moz-element()
+ /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+];
+
+// Maximum number of rows to display in console.table().
+const TABLE_ROW_MAX_ITEMS = 1000;
+
+// Maximum number of columns to display in console.table().
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+/**
+ * The ConsoleOutput object is used to manage output of messages in the Web
+ * Console.
+ *
+ * @constructor
+ * @param object owner
+ * The console output owner. This usually the WebConsoleFrame instance.
+ * Any other object can be used, as long as it has the following
+ * properties and methods:
+ * - window
+ * - document
+ * - outputMessage(category, methodOrNode[, methodArguments])
+ * TODO: this is needed temporarily, until bug 778766 is fixed.
+ */
+function ConsoleOutput(owner)
+{
+ this.owner = owner;
+ this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
+}
+
+ConsoleOutput.prototype = {
+ _dummyElement: null,
+
+ /**
+ * The output container.
+ * @type DOMElement
+ */
+ get element() {
+ return this.owner.outputNode;
+ },
+
+ /**
+ * The document that holds the output.
+ * @type DOMDocument
+ */
+ get document() {
+ return this.owner ? this.owner.document : null;
+ },
+
+ /**
+ * The DOM window that holds the output.
+ * @type Window
+ */
+ get window() {
+ return this.owner.window;
+ },
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.owner.webConsoleClient;
+ },
+
+ /**
+ * Getter for the current toolbox debuggee target.
+ * @type Target
+ */
+ get toolboxTarget() {
+ return this.owner.owner.target;
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actorId
+ * The actor ID you want to release.
+ */
+ _releaseObject: function(actorId)
+ {
+ this.owner._releaseObject(actorId);
+ },
+
+ /**
+ * Add a message to output.
+ *
+ * @param object ...args
+ * Any number of Message objects.
+ * @return this
+ */
+ addMessage: function(...args)
+ {
+ for (let msg of args) {
+ msg.init(this);
+ this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
+ [msg]);
+ }
+ return this;
+ },
+
+ /**
+ * Message renderer used for compatibility with the current Web Console output
+ * implementation. This method is invoked for every message object that is
+ * flushed to output. The message object is initialized and rendered, then it
+ * is displayed.
+ *
+ * TODO: remove this method once bug 778766 is fixed.
+ *
+ * @private
+ * @param object message
+ * The message object to render.
+ * @return DOMElement
+ * The message DOM element that can be added to the console output.
+ */
+ _onFlushOutputMessage: function(message)
+ {
+ return message.render().element;
+ },
+
+ /**
+ * Get an array of selected messages. This list is based on the text selection
+ * start and end points.
+ *
+ * @param number [limit]
+ * Optional limit of selected messages you want. If no value is given,
+ * all of the selected messages are returned.
+ * @return array
+ * Array of DOM elements for each message that is currently selected.
+ */
+ getSelectedMessages: function(limit)
+ {
+ let selection = this.window.getSelection();
+ if (selection.isCollapsed) {
+ return [];
+ }
+
+ if (selection.containsNode(this.element, true)) {
+ return Array.slice(this.element.children);
+ }
+
+ let anchor = this.getMessageForElement(selection.anchorNode);
+ let focus = this.getMessageForElement(selection.focusNode);
+ if (!anchor || !focus) {
+ return [];
+ }
+
+ let start, end;
+ if (anchor.timestamp > focus.timestamp) {
+ start = focus;
+ end = anchor;
+ } else {
+ start = anchor;
+ end = focus;
+ }
+
+ let result = [];
+ let current = start;
+ while (current) {
+ result.push(current);
+ if (current == end || (limit && result.length == limit)) {
+ break;
+ }
+ current = current.nextSibling;
+ }
+ return result;
+ },
+
+ /**
+ * Find the DOM element of a message for any given descendant.
+ *
+ * @param DOMElement elem
+ * The element to start the search from.
+ * @return DOMElement|null
+ * The DOM element of the message, if any.
+ */
+ getMessageForElement: function(elem)
+ {
+ while (elem && elem.parentNode) {
+ if (elem.classList && elem.classList.contains("message")) {
+ return elem;
+ }
+ elem = elem.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Select all messages.
+ */
+ selectAllMessages: function()
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(this.element);
+ selection.addRange(range);
+ },
+
+ /**
+ * Add a message to the selection.
+ *
+ * @param DOMElement elem
+ * The message element to select.
+ */
+ selectMessage: function(elem)
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(elem);
+ selection.addRange(range);
+ },
+
+ /**
+ * Open an URL in a new tab.
+ * @see WebConsole.openLink() in hudservice.js
+ */
+ openLink: function()
+ {
+ this.owner.owner.openLink.apply(this.owner.owner, arguments);
+ },
+
+ /**
+ * Open the variables view to inspect an object actor.
+ * @see JSTerm.openVariablesView() in webconsole.js
+ */
+ openVariablesView: function()
+ {
+ this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
+ },
+
+ /**
+ * Destroy this ConsoleOutput instance.
+ */
+ destroy: function()
+ {
+ this._dummyElement = null;
+ this.owner = null;
+ },
+}; // ConsoleOutput.prototype
+
+/**
+ * Message objects container.
+ * @type object
+ */
+let Messages = {};
+
+/**
+ * The BaseMessage object is used for all types of messages. Every kind of
+ * message should use this object as its base.
+ *
+ * @constructor
+ */
+Messages.BaseMessage = function()
+{
+ this.widgets = new Set();
+ this._onClickAnchor = this._onClickAnchor.bind(this);
+ this._repeatID = { uid: gSequenceId() };
+ this.textContent = "";
+};
+
+Messages.BaseMessage.prototype = {
+ /**
+ * Reference to the ConsoleOutput owner.
+ *
+ * @type object|null
+ * This is |null| if the message is not yet initialized.
+ */
+ output: null,
+
+ /**
+ * Reference to the parent message object, if this message is in a group or if
+ * it is otherwise owned by another message.
+ *
+ * @type object|null
+ */
+ parent: null,
+
+ /**
+ * Message DOM element.
+ *
+ * @type DOMElement|null
+ * This is |null| if the message is not yet rendered.
+ */
+ element: null,
+
+ /**
+ * Tells if this message is visible or not.
+ * @type boolean
+ */
+ get visible() {
+ return this.element && this.element.parentNode;
+ },
+
+ /**
+ * The owner DOM document.
+ * @type DOMElement
+ */
+ get document() {
+ return this.output.document;
+ },
+
+ /**
+ * Holds the text-only representation of the message.
+ * @type string
+ */
+ textContent: null,
+
+ /**
+ * Set of widgets included in this message.
+ * @type Set
+ */
+ widgets: null,
+
+ // Properties that allow compatibility with the current Web Console output
+ // implementation.
+ _categoryCompat: null,
+ _severityCompat: null,
+ _categoryNameCompat: null,
+ _severityNameCompat: null,
+ _filterKeyCompat: null,
+
+ /**
+ * Object that is JSON-ified and used as a non-unique ID for tracking
+ * duplicate messages.
+ * @private
+ * @type object
+ */
+ _repeatID: null,
+
+ /**
+ * Initialize the message.
+ *
+ * @param object output
+ * The ConsoleOutput owner.
+ * @param object [parent=null]
+ * Optional: a different message object that owns this instance.
+ * @return this
+ */
+ init: function(output, parent=null)
+ {
+ this.output = output;
+ this.parent = parent;
+ return this;
+ },
+
+ /**
+ * Non-unique ID for this message object used for tracking duplicate messages.
+ * Different message kinds can identify themselves based their own criteria.
+ *
+ * @return string
+ */
+ getRepeatID: function()
+ {
+ return JSON.stringify(this._repeatID);
+ },
+
+ /**
+ * Render the message. After this method is invoked the |element| property
+ * will point to the DOM element of this message.
+ * @return this
+ */
+ render: function()
+ {
+ if (!this.element) {
+ this.element = this._renderCompat();
+ }
+ return this;
+ },
+
+ /**
+ * Prepare the message container for the Web Console, such that it is
+ * compatible with the current implementation.
+ * TODO: remove this once bug 778766 is fixed.
+ *
+ * @private
+ * @return Element
+ * The DOM element that wraps the message.
+ */
+ _renderCompat: function()
+ {
+ let doc = this.output.document;
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "console-msg-" + gSequenceId();
+ container.className = "message";
+ if (this.category == "input") {
+ // Assistive technology tools shouldn't echo input to the user,
+ // as the user knows what they've just typed.
+ container.setAttribute("aria-live", "off");
+ }
+ container.category = this._categoryCompat;
+ container.severity = this._severityCompat;
+ container.setAttribute("category", this._categoryNameCompat);
+ container.setAttribute("severity", this._severityNameCompat);
+ container.setAttribute("filter", this._filterKeyCompat);
+ container.clipboardText = this.textContent;
+ container.timestamp = this.timestamp;
+ container._messageObject = this;
+
+ return container;
+ },
+
+ /**
+ * Add a click callback to a given DOM element.
+ *
+ * @private
+ * @param Element element
+ * The DOM element to which you want to add a click event handler.
+ * @param function [callback=this._onClickAnchor]
+ * Optional click event handler. The default event handler is
+ * |this._onClickAnchor|.
+ */
+ _addLinkCallback: function(element, callback = this._onClickAnchor)
+ {
+ // This is going into the WebConsoleFrame object instance that owns
+ // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
+ // object instance from hudservice.js.
+ // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
+ // is fixed.
+ this.output.owner._addMessageLinkCallback(element, callback);
+ },
+
+ /**
+ * The default |click| event handler for links in the output. This function
+ * opens the anchor's link in a new tab.
+ *
+ * @private
+ * @param Event event
+ * The DOM event that invoked this function.
+ */
+ _onClickAnchor: function(event)
+ {
+ this.output.openLink(event.target.href);
+ },
+
+ destroy: function()
+ {
+ // Destroy all widgets that have registered themselves in this.widgets
+ for (let widget of this.widgets) {
+ widget.destroy();
+ }
+ this.widgets.clear();
+ }
+}; // Messages.BaseMessage.prototype
+
+
+/**
+ * The NavigationMarker is used to show a page load event.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param object response
+ * The response received from the back end.
+ * @param number timestamp
+ * The message date and time, milliseconds elapsed since 1 January 1970
+ * 00:00:00 UTC.
+ */
+Messages.NavigationMarker = function(response, timestamp)
+{
+ Messages.BaseMessage.call(this);
+
+ // Store the response packet received from the server. It might
+ // be useful for extensions customizing the console output.
+ this.response = response;
+ this._url = response.url;
+ this.textContent = "------ " + this._url;
+ this.timestamp = timestamp;
+};
+
+Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype,
+{
+ /**
+ * The address of the loading page.
+ * @private
+ * @type string
+ */
+ _url: null,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ _categoryCompat: COMPAT.CATEGORIES.NETWORK,
+ _severityCompat: COMPAT.SEVERITIES.LOG,
+ _categoryNameCompat: "network",
+ _severityNameCompat: "info",
+ _filterKeyCompat: "networkinfo",
+
+ /**
+ * Prepare the DOM element for this message.
+ * @return this
+ */
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let url = this._url;
+ let pos = url.indexOf("?");
+ if (pos > -1) {
+ url = url.substr(0, pos);
+ }
+
+ let doc = this.output.document;
+ let urlnode = doc.createElementNS(XHTML_NS, "a");
+ urlnode.className = "url";
+ urlnode.textContent = url;
+ urlnode.title = this._url;
+ urlnode.href = this._url;
+ urlnode.draggable = false;
+ this._addLinkCallback(urlnode);
+
+ let render = Messages.BaseMessage.prototype.render.bind(this);
+ render().element.appendChild(urlnode);
+ this.element.classList.add("navigation-marker");
+ this.element.url = this._url;
+ this.element.appendChild(doc.createTextNode("\n"));
+
+ return this;
+ },
+}); // Messages.NavigationMarker.prototype
+
+
+/**
+ * The Simple message is used to show any basic message in the Web Console.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param string|Node|function message
+ * The message to display.
+ * @param object [options]
+ * Options for this message:
+ * - category: (string) category that this message belongs to. Defaults
+ * to no category.
+ * - severity: (string) severity of the message. Defaults to no severity.
+ * - timestamp: (number) date and time when the message was recorded.
+ * Defaults to |Date.now()|.
+ * - link: (string) if provided, the message will be wrapped in an anchor
+ * pointing to the given URL here.
+ * - linkCallback: (function) if provided, the message will be wrapped in
+ * an anchor. The |linkCallback| function will be added as click event
+ * handler.
+ * - location: object that tells the message source: url, line, column
+ * and lineText.
+ * - className: (string) additional element class names for styling
+ * purposes.
+ * - private: (boolean) mark this as a private message.
+ * - filterDuplicates: (boolean) true if you do want this message to be
+ * filtered as a potential duplicate message, false otherwise.
+ */
+Messages.Simple = function(message, options = {})
+{
+ Messages.BaseMessage.call(this);
+
+ this.category = options.category;
+ this.severity = options.severity;
+ this.location = options.location;
+ this.timestamp = options.timestamp || Date.now();
+ this.private = !!options.private;
+
+ this._message = message;
+ this._className = options.className;
+ this._link = options.link;
+ this._linkCallback = options.linkCallback;
+ this._filterDuplicates = options.filterDuplicates;
+};
+
+Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
+{
+ /**
+ * Message category.
+ * @type string
+ */
+ category: null,
+
+ /**
+ * Message severity.
+ * @type string
+ */
+ severity: null,
+
+ /**
+ * Message source location. Properties: url, line, column, lineText.
+ * @type object
+ */
+ location: null,
+
+ /**
+ * Tells if this message comes from a private browsing context.
+ * @type boolean
+ */
+ private: false,
+
+ /**
+ * Custom class name for the DOM element of the message.
+ * @private
+ * @type string
+ */
+ _className: null,
+
+ /**
+ * Message link - if this message is clicked then this URL opens in a new tab.
+ * @private
+ * @type string
+ */
+ _link: null,
+
+ /**
+ * Message click event handler.
+ * @private
+ * @type function
+ */
+ _linkCallback: null,
+
+ /**
+ * Tells if this message should be checked if it is a duplicate of another
+ * message or not.
+ */
+ _filterDuplicates: false,
+
+ /**
+ * The raw message displayed by this Message object. This can be a function,
+ * DOM node or a string.
+ *
+ * @private
+ * @type mixed
+ */
+ _message: null,
+
+ _afterMessage: null,
+ _objectActors: null,
+ _groupDepthCompat: 0,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ get _categoryCompat() {
+ return this.category ?
+ COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
+ },
+ get _severityCompat() {
+ return this.severity ?
+ COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
+ },
+ get _categoryNameCompat() {
+ return this.category ?
+ COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
+ },
+ get _severityNameCompat() {
+ return this.severity ?
+ COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
+ },
+
+ get _filterKeyCompat() {
+ return this._categoryCompat !== null && this._severityCompat !== null ?
+ COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
+ null;
+ },
+
+ init: function()
+ {
+ Messages.BaseMessage.prototype.init.apply(this, arguments);
+ this._groupDepthCompat = this.output.owner.groupDepth;
+ this._initRepeatID();
+ return this;
+ },
+
+ _initRepeatID: function()
+ {
+ if (!this._filterDuplicates) {
+ return;
+ }
+
+ // Add the properties we care about for identifying duplicate messages.
+ let rid = this._repeatID;
+ delete rid.uid;
+
+ rid.category = this.category;
+ rid.severity = this.severity;
+ rid.private = this.private;
+ rid.location = this.location;
+ rid.link = this._link;
+ rid.linkCallback = this._linkCallback + "";
+ rid.className = this._className;
+ rid.groupDepth = this._groupDepthCompat;
+ rid.textContent = "";
+ },
+
+ getRepeatID: function()
+ {
+ // No point in returning a string that includes other properties when there
+ // is a unique ID.
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ return JSON.stringify(this._repeatID);
+ },
+
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
+
+ let icon = this.document.createElementNS(XHTML_NS, "span");
+ icon.className = "icon";
+ icon.title = l10n.getStr("severity." + this._severityNameCompat);
+
+ // Apply the current group by indenting appropriately.
+ // TODO: remove this once bug 778766 is fixed.
+ let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+ indentNode.style.width = indent + "px";
+
+ let body = this._renderBody();
+ this._repeatID.textContent += "|" + body.textContent;
+
+ let repeatNode = this._renderRepeatNode();
+ let location = this._renderLocation();
+
+ Messages.BaseMessage.prototype.render.call(this);
+ if (this._className) {
+ this.element.className += " " + this._className;
+ }
+
+ this.element.appendChild(timestamp.element);
+ this.element.appendChild(indentNode);
+ this.element.appendChild(icon);
+ this.element.appendChild(body);
+ if (repeatNode) {
+ this.element.appendChild(repeatNode);
+ }
+ if (location) {
+ this.element.appendChild(location);
+ }
+ this.element.appendChild(this.document.createTextNode("\n"));
+
+ this.element.clipboardText = this.element.textContent;
+
+ if (this.private) {
+ this.element.setAttribute("private", true);
+ }
+
+ if (this._afterMessage) {
+ this.element._outputAfterNode = this._afterMessage.element;
+ this._afterMessage = null;
+ }
+
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ this.element._objectActors = this._objectActors;
+ this._objectActors = null;
+
+ return this;
+ },
+
+ /**
+ * Render the message body DOM element.
+ * @private
+ * @return Element
+ */
+ _renderBody: function()
+ {
+ let body = this.document.createElementNS(XHTML_NS, "span");
+ body.className = "message-body-wrapper message-body devtools-monospace";
+
+ let bodyInner = this.document.createElementNS(XHTML_NS, "span");
+ body.appendChild(bodyInner);
+
+ let anchor, container = bodyInner;
+ if (this._link || this._linkCallback) {
+ container = anchor = this.document.createElementNS(XHTML_NS, "a");
+ anchor.href = this._link || "#";
+ anchor.draggable = false;
+ this._addLinkCallback(anchor, this._linkCallback);
+ bodyInner.appendChild(anchor);
+ }
+
+ if (typeof this._message == "function") {
+ container.appendChild(this._message(this));
+ } else if (this._message instanceof Ci.nsIDOMNode) {
+ container.appendChild(this._message);
+ } else {
+ container.textContent = this._message;
+ }
+
+ return body;
+ },
+
+ /**
+ * Render the repeat bubble DOM element part of the message.
+ * @private
+ * @return Element
+ */
+ _renderRepeatNode: function()
+ {
+ if (!this._filterDuplicates) {
+ return null;
+ }
+
+ let repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = this.getRepeatID();
+ return repeatNode;
+ },
+
+ /**
+ * Render the message source location DOM element.
+ * @private
+ * @return Element
+ */
+ _renderLocation: function()
+ {
+ if (!this.location) {
+ return null;
+ }
+
+ let {url, line, column} = this.location;
+ if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
+ return null;
+ }
+
+ // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
+ // TODO: move createLocationNode() into this file when bug 778766 is fixed.
+ return this.output.owner.createLocationNode({url: url,
+ line: line,
+ column: column});
+ },
+}); // Messages.Simple.prototype
+
+
+/**
+ * The Extended message.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param array messagePieces
+ * The message to display given as an array of elements. Each array
+ * element can be a DOM node, function, ObjectActor, LongString or
+ * a string.
+ * @param object [options]
+ * Options for rendering this message:
+ * - quoteStrings: boolean that tells if you want strings to be wrapped
+ * in quotes or not.
+ */
+Messages.Extended = function(messagePieces, options = {})
+{
+ Messages.Simple.call(this, null, options);
+
+ this._messagePieces = messagePieces;
+
+ if ("quoteStrings" in options) {
+ this._quoteStrings = options.quoteStrings;
+ }
+
+ this._repeatID.quoteStrings = this._quoteStrings;
+ this._repeatID.messagePieces = messagePieces + "";
+ this._repeatID.actors = new Set(); // using a set to avoid duplicates
+};
+
+Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
+{
+ /**
+ * The message pieces displayed by this message instance.
+ * @private
+ * @type array
+ */
+ _messagePieces: null,
+
+ /**
+ * Boolean that tells if the strings displayed in this message are wrapped.
+ * @private
+ * @type boolean
+ */
+ _quoteStrings: true,
+
+ getRepeatID: function()
+ {
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ // Sets are not stringified correctly. Temporarily switching to an array.
+ let actors = this._repeatID.actors;
+ this._repeatID.actors = [...actors];
+ let result = JSON.stringify(this._repeatID);
+ this._repeatID.actors = actors;
+ return result;
+ },
+
+ render: function()
+ {
+ let result = this.document.createDocumentFragment();
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
+ if (separator) {
+ result.appendChild(separator);
+ }
+
+ let piece = this._messagePieces[i];
+ result.appendChild(this._renderBodyPiece(piece));
+ }
+
+ this._message = result;
+ this._messagePieces = null;
+ return Messages.Simple.prototype.render.call(this);
+ },
+
+ /**
+ * Render the separator between the pieces of the message.
+ *
+ * @private
+ * @return Element
+ */
+ _renderBodyPieceSeparator: function() { return null; },
+
+ /**
+ * Render one piece/element of the message array.
+ *
+ * @private
+ * @param mixed piece
+ * Message element to display - this can be a LongString, ObjectActor,
+ * DOM node or a function to invoke.
+ * @return Element
+ */
+ _renderBodyPiece: function(piece)
+ {
+ if (piece instanceof Ci.nsIDOMNode) {
+ return piece;
+ }
+ if (typeof piece == "function") {
+ return piece(this);
+ }
+
+ return this._renderValueGrip(piece);
+ },
+
+ /**
+ * Render a grip that represents a value received from the server. This method
+ * picks the appropriate widget to render the value with.
+ *
+ * @private
+ * @param object grip
+ * The value grip received from the server.
+ * @param object options
+ * Options for displaying the value. Available options:
+ * - noStringQuotes - boolean that tells the renderer to not use quotes
+ * around strings.
+ * - concise - boolean that tells the renderer to compactly display the
+ * grip. This is typically set to true when the object needs to be
+ * displayed in an array preview, or as a property value in object
+ * previews, etc.
+ * @return DOMElement
+ * The DOM element that displays the given grip.
+ */
+ _renderValueGrip: function(grip, options = {})
+ {
+ let isPrimitive = VariablesView.isPrimitive({ value: grip });
+ let isActorGrip = WebConsoleUtils.isActorGrip(grip);
+ let noStringQuotes = !this._quoteStrings;
+ if ("noStringQuotes" in options) {
+ noStringQuotes = options.noStringQuotes;
+ }
+
+ if (isActorGrip) {
+ this._repeatID.actors.add(grip.actor);
+
+ if (!isPrimitive) {
+ return this._renderObjectActor(grip, options);
+ }
+ if (grip.type == "longString") {
+ let widget = new Widgets.LongString(this, grip, options).render();
+ return widget.element;
+ }
+ }
+
+ let result = this.document.createElementNS(XHTML_NS, "span");
+ if (isPrimitive) {
+ if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) {
+ let widget = new Widgets.URLString(this, grip, options).render();
+ return widget.element;
+ }
+
+ let className = this.getClassNameForValueGrip(grip);
+ if (className) {
+ result.className = className;
+ }
+
+ result.textContent = VariablesView.getString(grip, {
+ noStringQuotes: noStringQuotes,
+ concise: options.concise,
+ });
+ } else {
+ result.textContent = grip;
+ }
+
+ return result;
+ },
+
+ /**
+ * Shorten grips of the type string, leaves other grips unmodified.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return object
+ * Possible values of object:
+ * - A shortened string, if original grip was of string type.
+ * - The unmodified input grip, if it wasn't of string type.
+ */
+ shortenValueGrip: function(grip)
+ {
+ let shortVal = grip;
+ if (typeof(grip)=="string") {
+ shortVal = grip.replace(/(\r\n|\n|\r)/gm," ");
+ if (shortVal.length > MAX_STRING_GRIP_LENGTH) {
+ shortVal = shortVal.substring(0,MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS;
+ }
+ }
+
+ return shortVal;
+ },
+
+ /**
+ * Get a CodeMirror-compatible class name for a given value grip.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return string
+ * The class name for the grip.
+ */
+ getClassNameForValueGrip: function(grip)
+ {
+ let map = {
+ "number": "cm-number",
+ "longstring": "console-string",
+ "string": "console-string",
+ "regexp": "cm-string-2",
+ "boolean": "cm-atom",
+ "-infinity": "cm-atom",
+ "infinity": "cm-atom",
+ "null": "cm-atom",
+ "undefined": "cm-comment",
+ "symbol": "cm-atom"
+ };
+
+ let className = map[typeof grip];
+ if (!className && grip && grip.type) {
+ className = map[grip.type.toLowerCase()];
+ }
+ if (!className && grip && grip.class) {
+ className = map[grip.class.toLowerCase()];
+ }
+
+ return className;
+ },
+
+ /**
+ * Display an object actor with the appropriate renderer.
+ *
+ * @private
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object options
+ * Options to use for displaying the ObjectActor.
+ * @see this._renderValueGrip for the available options.
+ * @return DOMElement
+ * The DOM element that displays the object actor.
+ */
+ _renderObjectActor: function(objectActor, options = {})
+ {
+ let widget = Widgets.ObjectRenderers.byClass[objectActor.class];
+
+ let { preview } = objectActor;
+ if ((!widget || (widget.canRender && !widget.canRender(objectActor)))
+ && preview
+ && preview.kind) {
+ widget = Widgets.ObjectRenderers.byKind[preview.kind];
+ }
+
+ if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
+ widget = Widgets.JSObject;
+ }
+
+ let instance = new widget(this, objectActor, options).render();
+ return instance.element;
+ },
+}); // Messages.Extended.prototype
+
+
+
+/**
+ * The JavaScriptEvalOutput message.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object evalResponse
+ * The evaluation response packet received from the server.
+ * @param string [errorMessage]
+ * Optional error message to display.
+ */
+Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage)
+{
+ let severity = "log", msg, quoteStrings = true;
+
+ // Store also the response packet from the back end. It might
+ // be useful to extensions customizing the console output.
+ this.response = evalResponse;
+
+ if (errorMessage) {
+ severity = "error";
+ msg = errorMessage;
+ quoteStrings = false;
+ } else {
+ msg = evalResponse.result;
+ }
+
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: evalResponse.timestamp,
+ category: "output",
+ severity: severity,
+ quoteStrings: quoteStrings,
+ };
+ Messages.Extended.call(this, [msg], options);
+};
+
+Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;
+
+/**
+ * The ConsoleGeneric message is used for console API calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleGeneric = function(packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ column: packet.columnNumber
+ },
+ };
+
+ switch (packet.level) {
+ case "count": {
+ let counter = packet.counter, label = counter.label;
+ if (!label) {
+ label = l10n.getStr("noCounterLabel");
+ }
+ Messages.Extended.call(this, [label+ ": " + counter.count], options);
+ break;
+ }
+ default:
+ Messages.Extended.call(this, packet.arguments, options);
+ break;
+ }
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._repeatID.styles = packet.styles;
+ this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
+ this._styles = packet.styles || [];
+
+ this._onClickCollapsible = this._onClickCollapsible.bind(this);
+};
+
+Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
+{
+ _styles: null,
+ _stacktrace: null,
+
+ /**
+ * Tells if the message can be expanded/collapsed.
+ * @type boolean
+ */
+ collapsible: false,
+
+ /**
+ * Getter that tells if this message is collapsed - no details are shown.
+ * @type boolean
+ */
+ get collapsed() {
+ return this.collapsible && this.element && !this.element.hasAttribute("open");
+ },
+
+ _renderBodyPieceSeparator: function()
+ {
+ return this.document.createTextNode(" ");
+ },
+
+ render: function()
+ {
+ let msg = this.document.createElementNS(XHTML_NS, "span");
+ msg.className = "message-body devtools-monospace";
+
+ this._renderBodyPieces(msg);
+
+ let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
+ let location = Messages.Simple.prototype._renderLocation.call(this);
+ if (location) {
+ location.target = "jsdebugger";
+ }
+
+ let stack = null;
+ let twisty = null;
+ if (this._stacktrace && this._stacktrace.length > 0) {
+ stack = new Widgets.Stacktrace(this, this._stacktrace).render().element;
+
+ twisty = this.document.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+ twisty.title = l10n.getStr("messageToggleDetails");
+ twisty.addEventListener("click", this._onClickCollapsible);
+ }
+
+ let flex = this.document.createElementNS(XHTML_NS, "span");
+ flex.className = "message-flex-body";
+
+ if (twisty) {
+ flex.appendChild(twisty);
+ }
+
+ flex.appendChild(msg);
+
+ if (repeatNode) {
+ flex.appendChild(repeatNode);
+ }
+ if (location) {
+ flex.appendChild(location);
+ }
+
+ let result = this.document.createDocumentFragment();
+ result.appendChild(flex);
+
+ if (stack) {
+ result.appendChild(this.document.createTextNode("\n"));
+ result.appendChild(stack);
+ }
+
+ this._message = result;
+ this._stacktrace = null;
+
+ Messages.Simple.prototype.render.call(this);
+
+ if (stack) {
+ this.collapsible = true;
+ this.element.setAttribute("collapsible", true);
+
+ let icon = this.element.querySelector(".icon");
+ icon.addEventListener("click", this._onClickCollapsible);
+ }
+
+ return this;
+ },
+
+ _renderBody: function()
+ {
+ let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
+ body.classList.remove("devtools-monospace", "message-body");
+ return body;
+ },
+
+ _renderBodyPieces: function(container)
+ {
+ let lastStyle = null;
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
+ if (separator) {
+ container.appendChild(separator);
+ }
+
+ let piece = this._messagePieces[i];
+ let style = this._styles[i];
+
+ // No long string support.
+ if (style && typeof style == "string" ) {
+ lastStyle = this.cleanupStyle(style);
+ }
+
+ container.appendChild(this._renderBodyPiece(piece, lastStyle));
+ }
+
+ this._messagePieces = null;
+ this._styles = null;
+ },
+
+ _renderBodyPiece: function(piece, style)
+ {
+ let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece);
+ let result = elem;
+
+ if (style) {
+ if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+ elem.style = style;
+ } else {
+ let span = this.document.createElementNS(XHTML_NS, "span");
+ span.style = style;
+ span.appendChild(elem);
+ result = span;
+ }
+ }
+
+ return result;
+ },
+
+ // no-op for the message location and .repeats elements.
+ // |this.render()| handles customized message output.
+ _renderLocation: function() { },
+ _renderRepeatNode: function() { },
+
+ /**
+ * Expand/collapse message details.
+ */
+ toggleDetails: function()
+ {
+ let twisty = this.element.querySelector(".theme-twisty");
+ if (this.element.hasAttribute("open")) {
+ this.element.removeAttribute("open");
+ twisty.removeAttribute("open");
+ } else {
+ this.element.setAttribute("open", true);
+ twisty.setAttribute("open", true);
+ }
+ },
+
+ /**
+ * The click event handler for the message expander arrow element. This method
+ * toggles the display of message details.
+ *
+ * @private
+ * @param nsIDOMEvent ev
+ * The DOM event object.
+ * @see this.toggleDetails()
+ */
+ _onClickCollapsible: function(ev)
+ {
+ ev.preventDefault();
+ this.toggleDetails();
+ },
+
+ /**
+ * Given a style attribute value, return a cleaned up version of the string
+ * such that:
+ *
+ * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
+ * - only some of the properties are allowed, based on a whitelist. See
+ * RE_ALLOWED_STYLES.
+ *
+ * @param string style
+ * The style string to cleanup.
+ * @return string
+ * The style value after cleanup.
+ */
+ cleanupStyle: function(style)
+ {
+ for (let r of RE_CLEANUP_STYLES) {
+ style = style.replace(r, "notallowed");
+ }
+
+ let dummy = this.output._dummyElement;
+ if (!dummy) {
+ dummy = this.output._dummyElement =
+ this.document.createElementNS(XHTML_NS, "div");
+ }
+ dummy.style = style;
+
+ let toRemove = [];
+ for (let i = 0; i < dummy.style.length; i++) {
+ let prop = dummy.style[i];
+ if (!RE_ALLOWED_STYLES.test(prop)) {
+ toRemove.push(prop);
+ }
+ }
+
+ for (let prop of toRemove) {
+ dummy.style.removeProperty(prop);
+ }
+
+ style = dummy.style.cssText;
+
+ dummy.style = "";
+
+ return style;
+ },
+}); // Messages.ConsoleGeneric.prototype
+
+/**
+ * The ConsoleTrace message is used for console.trace() calls.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTrace = function(packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ this._renderStack = this._renderStack.bind(this);
+ Messages.Simple.call(this, this._renderStack, options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype,
+{
+ /**
+ * Holds the stackframes received from the server.
+ *
+ * @private
+ * @type array
+ */
+ _stacktrace: null,
+
+ /**
+ * Holds the arguments the content script passed to the console.trace()
+ * method. This array is cleared when the message is initialized, and
+ * associated actors are released.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ init: function()
+ {
+ let result = Messages.Simple.prototype.init.apply(this, arguments);
+
+ // We ignore console.trace() arguments. Release object actors.
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+
+ return result;
+ },
+
+ render: function()
+ {
+ Messages.Simple.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ /**
+ * Render the stack frames.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderStack: function()
+ {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "trace";
+
+ let title = this.document.createElementNS(XHTML_NS, "span");
+ title.className = "message-body devtools-monospace";
+ title.appendChild(cmvar);
+ title.appendChild(this.document.createTextNode("."));
+ title.appendChild(cmprop);
+ title.appendChild(this.document.createTextNode("():"));
+
+ let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
+ let location = Messages.Simple.prototype._renderLocation.call(this);
+ if (location) {
+ location.target = "jsdebugger";
+ }
+
+ let widget = new Widgets.Stacktrace(this, this._stacktrace).render();
+
+ let body = this.document.createElementNS(XHTML_NS, "span");
+ body.className = "message-flex-body";
+ body.appendChild(title);
+ if (repeatNode) {
+ body.appendChild(repeatNode);
+ }
+ if (location) {
+ body.appendChild(location);
+ }
+ body.appendChild(this.document.createTextNode("\n"));
+
+ let frag = this.document.createDocumentFragment();
+ frag.appendChild(body);
+ frag.appendChild(widget.element);
+
+ return frag;
+ },
+
+ _renderBody: function()
+ {
+ let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
+ body.classList.remove("devtools-monospace", "message-body");
+ return body;
+ },
+
+ // no-op for the message location and .repeats elements.
+ // |this._renderStack| handles customized message output.
+ _renderLocation: function() { },
+ _renderRepeatNode: function() { },
+}); // Messages.ConsoleTrace.prototype
+
+/**
+ * The ConsoleTable message is used for console.table() calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTable = function(packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: false,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ this._populateTableData = this._populateTableData.bind(this);
+ this._renderTable = this._renderTable.bind(this);
+ Messages.Extended.call(this, [this._renderTable], options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype,
+{
+ /**
+ * Holds the arguments the content script passed to the console.table()
+ * method.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ /**
+ * Array of objects that holds the data to log in the table.
+ *
+ * @private
+ * @type array
+ */
+ _data: null,
+
+ /**
+ * Key value pair of the id and display name for the columns in the table.
+ * Refer to the TableWidget API.
+ *
+ * @private
+ * @type object
+ */
+ _columns: null,
+
+ /**
+ * A promise that resolves when the table data is ready or null if invalid
+ * arguments are provided.
+ *
+ * @private
+ * @type promise|null
+ */
+ _populatePromise: null,
+
+ init: function()
+ {
+ let result = Messages.Extended.prototype.init.apply(this, arguments);
+ this._data = [];
+ this._columns = {};
+
+ this._populatePromise = this._populateTableData();
+
+ return result;
+ },
+
+ /**
+ * Sets the key value pair of the id and display name for the columns in the
+ * table.
+ *
+ * @private
+ * @param array|string columns
+ * Either a string or array containing the names for the columns in
+ * the output table.
+ */
+ _setColumns: function(columns)
+ {
+ if (columns.class == "Array") {
+ let items = columns.preview.items;
+
+ for (let item of items) {
+ if (typeof item == "string") {
+ this._columns[item] = item;
+ }
+ }
+ } else if (typeof columns == "string" && columns) {
+ this._columns[columns] = columns;
+ }
+ },
+
+ /**
+ * Retrieves the table data and columns from the arguments received from the
+ * server.
+ *
+ * @return Promise|null
+ * Returns a promise that resolves when the table data is ready or
+ * null if the arguments are invalid.
+ */
+ _populateTableData: function()
+ {
+ let deferred = promise.defer();
+
+ if (this._arguments.length <= 0) {
+ return;
+ }
+
+ let data = this._arguments[0];
+ if (data.class != "Array" && data.class != "Object" &&
+ data.class != "Map" && data.class != "Set") {
+ return;
+ }
+
+ let hasColumnsArg = false;
+ if (this._arguments.length > 1) {
+ if (data.class == "Object" || data.class == "Array") {
+ this._columns["_index"] = l10n.getStr("table.index");
+ } else {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ }
+
+ this._setColumns(this._arguments[1]);
+ hasColumnsArg = true;
+ }
+
+ if (data.class == "Object" || data.class == "Array") {
+ // Get the object properties, and parse the key and value properties into
+ // the table data and columns.
+ this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client,
+ data);
+ this.client.getPrototypeAndProperties(aResponse => {
+ let {ownProperties} = aResponse;
+ let rowCount = 0;
+ let columnCount = 0;
+
+ for (let index of Object.keys(ownProperties || {})) {
+ // Avoid outputting the length property if the data argument provided
+ // is an array
+ if (data.class == "Array" && index == "length") {
+ continue;
+ }
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.index");
+ }
+
+ if (data.class == "Array") {
+ if (index == parseInt(index)) {
+ index = parseInt(index);
+ }
+ }
+
+ let property = ownProperties[index].value;
+ let item = { _index: index };
+
+ if (property.class == "Object" || property.class == "Array") {
+ let {preview} = property;
+ let entries = property.class == "Object" ?
+ preview.ownProperties : preview.items;
+
+ for (let key of Object.keys(entries)) {
+ let value = property.class == "Object" ?
+ preview.ownProperties[key].value : preview.items[key];
+
+ item[key] = this._renderValueGrip(value, { concise: true });
+
+ if (!hasColumnsArg && !(key in this._columns) &&
+ (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) {
+ this._columns[key] = key;
+ }
+ }
+ } else {
+ // Display the value for any non-object data input.
+ item["_value"] = this._renderValueGrip(property, { concise: true });
+
+ if (!hasColumnsArg && !("_value" in this._columns)) {
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+ }
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ });
+ } else if (data.class == "Map") {
+ let entries = data.preview.entries;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_key"] = l10n.getStr("table.key");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let [key, value] of entries) {
+ let item = {
+ _index: rowCount,
+ _key: this._renderValueGrip(key, { concise: true }),
+ _value: this._renderValueGrip(value, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ } else if (data.class == "Set") {
+ let entries = data.preview.items;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let entry of entries) {
+ let item = {
+ _index : rowCount,
+ _value: this._renderValueGrip(entry, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ },
+
+ render: function()
+ {
+ Messages.Extended.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ /**
+ * Render the table.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderTable: function()
+ {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "table";
+
+ let title = this.document.createElementNS(XHTML_NS, "span");
+ title.className = "message-body devtools-monospace";
+ title.appendChild(cmvar);
+ title.appendChild(this.document.createTextNode("."));
+ title.appendChild(cmprop);
+ title.appendChild(this.document.createTextNode("():"));
+
+ let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
+ let location = Messages.Simple.prototype._renderLocation.call(this);
+ if (location) {
+ location.target = "jsdebugger";
+ }
+
+ let body = this.document.createElementNS(XHTML_NS, "span");
+ body.className = "message-flex-body";
+ body.appendChild(title);
+ if (repeatNode) {
+ body.appendChild(repeatNode);
+ }
+ if (location) {
+ body.appendChild(location);
+ }
+ body.appendChild(this.document.createTextNode("\n"));
+
+ let result = this.document.createElementNS(XHTML_NS, "div");
+ result.appendChild(body);
+
+ if (this._populatePromise) {
+ this._populatePromise.then(() => {
+ if (this._data.length > 0) {
+ let widget = new Widgets.Table(this, this._data, this._columns).render();
+ result.appendChild(widget.element);
+ }
+
+ result.scrollIntoView();
+ this.output.owner.emit("messages-table-rendered");
+
+ // Release object actors
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+ });
+ }
+
+ return result;
+ },
+
+ _renderBody: function()
+ {
+ let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
+ body.classList.remove("devtools-monospace", "message-body");
+ return body;
+ },
+
+ // no-op for the message location and .repeats elements.
+ // |this._renderTable| handles customized message output.
+ _renderLocation: function() { },
+ _renderRepeatNode: function() { },
+}); // Messages.ConsoleTable.prototype
+
+let Widgets = {};
+
+/**
+ * The base widget class.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ */
+Widgets.BaseWidget = function(message)
+{
+ this.message = message;
+};
+
+Widgets.BaseWidget.prototype = {
+ /**
+ * The owning message object.
+ * @type object
+ */
+ message: null,
+
+ /**
+ * The DOM element of the rendered widget.
+ * @type Element
+ */
+ element: null,
+
+ /**
+ * Getter for the DOM document that holds the output.
+ * @type Document
+ */
+ get document() {
+ return this.message.document;
+ },
+
+ /**
+ * The ConsoleOutput instance that owns this widget instance.
+ */
+ get output() {
+ return this.message.output;
+ },
+
+ /**
+ * Render the widget DOM element.
+ * @return this
+ */
+ render: function() { },
+
+ /**
+ * Destroy this widget instance.
+ */
+ destroy: function() { },
+
+ /**
+ * Helper for creating DOM elements for widgets.
+ *
+ * Usage:
+ * this.el("tag#id.class.names"); // create element "tag" with ID "id" and
+ * two class names, .class and .names.
+ *
+ * this.el("span", { attr1: "value1", ... }) // second argument can be an
+ * object that holds element attributes and values for the new DOM element.
+ *
+ * this.el("p", { attr1: "value1", ... }, "text content"); // the third
+ * argument can include the default .textContent of the new DOM element.
+ *
+ * this.el("p", "text content"); // if the second argument is not an object,
+ * it will be used as .textContent for the new DOM element.
+ *
+ * @param string tagNameIdAndClasses
+ * Tag name for the new element, optionally followed by an ID and/or
+ * class names. Examples: "span", "div#fooId", "div.class.names",
+ * "p#id.class".
+ * @param string|object [attributesOrTextContent]
+ * If this argument is an object it will be used to set the attributes
+ * of the new DOM element. Otherwise, the value becomes the
+ * .textContent of the new DOM element.
+ * @param string [textContent]
+ * If this argument is provided the value is used as the textContent of
+ * the new DOM element.
+ * @return DOMElement
+ * The new DOM element.
+ */
+ el: function(tagNameIdAndClasses)
+ {
+ let attrs, text;
+ if (typeof arguments[1] == "object") {
+ attrs = arguments[1];
+ text = arguments[2];
+ } else {
+ text = arguments[1];
+ }
+
+ let tagName = tagNameIdAndClasses.split(/#|\./)[0];
+
+ let elem = this.document.createElementNS(XHTML_NS, tagName);
+ for (let name of Object.keys(attrs || {})) {
+ elem.setAttribute(name, attrs[name]);
+ }
+ if (text !== undefined && text !== null) {
+ elem.textContent = text;
+ }
+
+ let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
+ for (let idOrClass of (idAndClasses || [])) {
+ if (idOrClass.charAt(0) == "#") {
+ elem.id = idOrClass.substr(1);
+ } else {
+ elem.classList.add(idOrClass.substr(1));
+ }
+ }
+
+ return elem;
+ },
+};
+
+/**
+ * The timestamp widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param number timestamp
+ * The UNIX timestamp to display.
+ */
+Widgets.MessageTimestamp = function(message, timestamp)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.timestamp = timestamp;
+};
+
+Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * The UNIX timestamp.
+ * @type number
+ */
+ timestamp: 0,
+
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ this.element = this.document.createElementNS(XHTML_NS, "span");
+ this.element.className = "timestamp devtools-monospace";
+ this.element.textContent = l10n.timestampString(this.timestamp) + " ";
+
+ return this;
+ },
+}); // Widgets.MessageTimestamp.prototype
+
+
+/**
+ * The URLString widget, for rendering strings where at least one token is a
+ * URL.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param string str
+ * The string, which contains at least one valid URL.
+ */
+Widgets.URLString = function(message, str)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.str = str;
+};
+
+Widgets.URLString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * The string to format, which contains at least one valid URL.
+ * @type string
+ */
+ str: "",
+
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ // The rendered URLString will be a <span> containing a number of text
+ // <spans> for non-URL tokens and <a>'s for URL tokens.
+ this.element = this.el("span", {
+ class: "console-string"
+ });
+ this.element.appendChild(this._renderText("\""));
+
+ // As we walk through the tokens of the source string, we make sure to preserve
+ // the original whitespace that seperated the tokens.
+ let tokens = this.str.split(/\s+/);
+ let textStart = 0;
+ let tokenStart;
+ for (let token of tokens) {
+ tokenStart = this.str.indexOf(token, textStart);
+ if (this._isURL(token)) {
+ this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart)));
+ textStart = tokenStart + token.length;
+ this.element.appendChild(this._renderURL(token));
+ }
+ }
+
+ // Clean up any non-URL text at the end of the source string.
+ this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length)));
+ this.element.appendChild(this._renderText("\""));
+
+ return this;
+ },
+
+ /**
+ * Determines whether a grip is a string containing a URL.
+ *
+ * @param string grip
+ * The grip, which may contain a URL.
+ * @return boolean
+ * Whether the grip is a string containing a URL.
+ */
+ containsURL: function(grip)
+ {
+ if (typeof grip != "string") {
+ return false;
+ }
+
+ let tokens = grip.split(/\s+/);
+ return tokens.some(this._isURL);
+ },
+
+ /**
+ * Determines whether a string token is a valid URL.
+ *
+ * @param string token
+ * The token.
+ * @return boolean
+ * Whenther the token is a URL.
+ */
+ _isURL: function(token) {
+ try {
+ let uri = URI.newURI(token, null, null);
+ let url = uri.QueryInterface(Ci.nsIURL);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Renders a string as a URL.
+ *
+ * @param string url
+ * The string to be rendered as a url.
+ * @return DOMElement
+ * An element containing the rendered string.
+ */
+ _renderURL: function(url)
+ {
+ let result = this.el("a", {
+ class: "url",
+ title: url,
+ href: url,
+ draggable: false
+ }, url);
+ this.message._addLinkCallback(result);
+ return result;
+ },
+
+ _renderText: function(text) {
+ return this.el("span", text);
+ },
+}); // Widgets.URLString.prototype
+
+/**
+ * Widget used for displaying ObjectActors that have no specialised renderers.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object [options]
+ * Options for displaying the given ObjectActor. See
+ * Messages.Extended.prototype._renderValueGrip for the available
+ * options.
+ */
+Widgets.JSObject = function(message, objectActor, options = {})
+{
+ Widgets.BaseWidget.call(this, message);
+ this.objectActor = objectActor;
+ this.options = options;
+ this._onClick = this._onClick.bind(this);
+};
+
+Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * The ObjectActor displayed by the widget.
+ * @type object
+ */
+ objectActor: null,
+
+ render: function()
+ {
+ if (!this.element) {
+ this._render();
+ }
+
+ return this;
+ },
+
+ _render: function()
+ {
+ let str = VariablesView.getString(this.objectActor, this.options);
+ let className = this.message.getClassNameForValueGrip(this.objectActor);
+ if (!className && this.objectActor.class == "Object") {
+ className = "cm-variable";
+ }
+
+ this.element = this._anchor(str, { className: className });
+ },
+
+ /**
+ * Render a concise representation of an object.
+ */
+ _renderConciseObject: function()
+ {
+ this.element = this._anchor(this.objectActor.class,
+ { className: "cm-variable" });
+ },
+
+ /**
+ * Render the `<class> { ` prefix of an object.
+ */
+ _renderObjectPrefix: function()
+ {
+ let { kind } = this.objectActor.preview;
+ this.element = this.el("span.kind-" + kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+ this._text(" { ");
+ },
+
+ /**
+ * Render the ` }` suffix of an object.
+ */
+ _renderObjectSuffix: function()
+ {
+ this._text(" }");
+ },
+
+ /**
+ * Render an object property.
+ *
+ * @param String key
+ * The property name.
+ * @param Object value
+ * The property value, as an RDP grip.
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ * @param Boolean valueIsText
+ * Add the value as is, don't treat it as a grip and pass it to
+ * `_renderValueGrip`.
+ */
+ _renderObjectProperty: function(key, value, container, needsComma, valueIsText = false)
+ {
+ if (needsComma) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ if (valueIsText) {
+ this._text(value);
+ } else {
+ let shortVal = this.message.shortenValueGrip(value);
+ let valueElem = this.message._renderValueGrip(shortVal, { concise: true });
+ container.appendChild(valueElem);
+ }
+ },
+
+ /**
+ * Render this object's properties.
+ *
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ */
+ _renderObjectProperties: function(container, needsComma)
+ {
+ let { preview } = this.objectActor;
+ let { ownProperties, safeGetterValues } = preview;
+
+ let shown = 0;
+
+ let getValue = desc => {
+ if (desc.get) {
+ return "Getter";
+ } else if (desc.set) {
+ return "Setter";
+ } else {
+ return desc.value;
+ }
+ };
+
+ for (let key of Object.keys(ownProperties || {})) {
+ this._renderObjectProperty(key, getValue(ownProperties[key]), container,
+ shown > 0 || needsComma,
+ ownProperties[key].get || ownProperties[key].set);
+ shown++;
+ }
+
+ let ownPropertiesShown = shown;
+
+ for (let key of Object.keys(safeGetterValues || {})) {
+ this._renderObjectProperty(key, safeGetterValues[key].getterValue,
+ container, shown > 0 || needsComma);
+ shown++;
+ }
+
+ if (typeof preview.ownPropertiesLength == "number" &&
+ ownPropertiesShown < preview.ownPropertiesLength) {
+ this._text(", ");
+
+ let n = preview.ownPropertiesLength - ownPropertiesShown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+ },
+
+ /**
+ * Render an anchor with a given text content and link.
+ *
+ * @private
+ * @param string text
+ * Text to show in the anchor.
+ * @param object [options]
+ * Available options:
+ * - onClick (function): "click" event handler.By default a click on
+ * the anchor opens the variables view for the current object actor
+ * (this.objectActor).
+ * - href (string): if given the string is used as a link, and clicks
+ * on the anchor open the link in a new tab.
+ * - appendTo (DOMElement): append the element to the given DOM
+ * element. If not provided, the anchor is appended to |this.element|
+ * if it is available. If |appendTo| is provided and if it is a falsy
+ * value, the anchor is not appended to any element.
+ * @return DOMElement
+ * The DOM element of the new anchor.
+ */
+ _anchor: function(text, options = {})
+ {
+ if (!options.onClick) {
+ // If the anchor has an URL, open it in a new tab. If not, show the
+ // current object actor.
+ options.onClick = options.href ? this._onClickAnchor : this._onClick;
+ }
+
+ let anchor = this.el("a", {
+ class: options.className,
+ draggable: false,
+ href: options.href || "#",
+ }, text);
+
+ this.message._addLinkCallback(anchor, options.onClick);
+
+ if (options.appendTo) {
+ options.appendTo.appendChild(anchor);
+ } else if (!("appendTo" in options) && this.element) {
+ this.element.appendChild(anchor);
+ }
+
+ return anchor;
+ },
+
+ /**
+ * The click event handler for objects shown inline.
+ * @private
+ */
+ _onClick: function()
+ {
+ this.output.openVariablesView({
+ label: VariablesView.getString(this.objectActor, { concise: true }),
+ objectActor: this.objectActor,
+ autofocus: true,
+ });
+ },
+
+ /**
+ * Add a string to the message.
+ *
+ * @private
+ * @param string str
+ * String to add.
+ * @param DOMElement [target = this.element]
+ * Optional DOM element to append the string to. The default is
+ * this.element.
+ */
+ _text: function(str, target = this.element)
+ {
+ target.appendChild(this.document.createTextNode(str));
+ },
+}); // Widgets.JSObject.prototype
+
+Widgets.ObjectRenderers = {};
+Widgets.ObjectRenderers.byKind = {};
+Widgets.ObjectRenderers.byClass = {};
+
+/**
+ * Add an object renderer.
+ *
+ * @param object obj
+ * An object that represents the renderer. Properties:
+ * - byClass (string, optional): this renderer will be used for the given
+ * object class.
+ * - byKind (string, optional): this renderer will be used for the given
+ * object kind.
+ * One of byClass or byKind must be provided.
+ * - extends (object, optional): the renderer object extends the given
+ * object. Default: Widgets.JSObject.
+ * - canRender (function, optional): this method is invoked when
+ * a candidate object needs to be displayed. The method is invoked as
+ * a static method, as such, none of the properties of the renderer
+ * object will be available. You get one argument: the object actor grip
+ * received from the server. If the method returns true, then this
+ * renderer is used for displaying the object, otherwise not.
+ * - initialize (function, optional): the constructor of the renderer
+ * widget. This function is invoked with the following arguments: the
+ * owner message object instance, the object actor grip to display, and
+ * an options object. See Messages.Extended.prototype._renderValueGrip()
+ * for details about the options object.
+ * - render (function, required): the method that displays the given
+ * object actor.
+ */
+Widgets.ObjectRenderers.add = function(obj)
+{
+ let extendObj = obj.extends || Widgets.JSObject;
+
+ let constructor = function() {
+ if (obj.initialize) {
+ obj.initialize.apply(this, arguments);
+ } else {
+ extendObj.apply(this, arguments);
+ }
+ };
+
+ let proto = WebConsoleUtils.cloneObject(obj, false, function(key) {
+ if (key == "initialize" || key == "canRender" ||
+ (key == "render" && extendObj === Widgets.JSObject)) {
+ return false;
+ }
+ return true;
+ });
+
+ if (extendObj === Widgets.JSObject) {
+ proto._render = obj.render;
+ }
+
+ constructor.canRender = obj.canRender;
+ constructor.prototype = Heritage.extend(extendObj.prototype, proto);
+
+ if (obj.byClass) {
+ Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
+ } else if (obj.byKind) {
+ Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
+ } else {
+ throw new Error("You are adding an object renderer without any byClass or " +
+ "byKind property.");
+ }
+};
+
+
+/**
+ * The widget used for displaying Date objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Date",
+
+ render: function()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ let anchorText = this.objectActor.class;
+ let anchorClass = "cm-variable";
+ if (preview && "timestamp" in preview && typeof preview.timestamp != "number") {
+ anchorText = new Date(preview.timestamp).toString(); // invalid date
+ anchorClass = "";
+ }
+
+ this._anchor(anchorText, { className: anchorClass });
+
+ if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") {
+ return;
+ }
+
+ this._text(" ");
+
+ let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
+ this.element.appendChild(elem);
+ },
+});
+
+/**
+ * The widget used for displaying Function objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Function",
+
+ render: function()
+ {
+ let grip = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+ let name = grip.userDisplayName || grip.displayName || grip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ let str = this.options.concise ? name || "function " : "function " + name;
+
+ if (this.options.concise) {
+ this._anchor(name || "function", {
+ className: name ? "cm-variable" : "cm-keyword",
+ });
+ if (!name) {
+ this._text(" ");
+ }
+ } else if (name) {
+ this.element.appendChild(this.el("span.cm-keyword", "function"));
+ this._text(" ");
+ this._anchor(name, { className: "cm-variable" });
+ } else {
+ this._anchor("function", { className: "cm-keyword" });
+ this._text(" ");
+ }
+
+ this._text("(");
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ let params = grip.parameterNames || [];
+ let shown = 0;
+ for (let param of params) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+ this.element.appendChild(this.el("span.cm-def", param));
+ shown++;
+ }
+
+ this._text(")");
+ },
+}); // Widgets.ObjectRenderers.byClass.Function
+
+/**
+ * The widget used for displaying ArrayLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ArrayLike",
+
+ render: function()
+ {
+ let {preview} = this.objectActor;
+ let {items} = preview;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!items || this.options.concise) {
+ this._text("[");
+ this.element.appendChild(this.el("span.cm-number", preview.length));
+ this._text("]");
+ return this;
+ }
+
+ this._text(" [ ");
+
+ let isFirst = true;
+ let emptySlots = 0;
+ // A helper that renders a comma between items if isFirst == false.
+ let renderSeparator = () => !isFirst && this._text(", ");
+
+ for (let item of items) {
+ if (item === null) {
+ emptySlots++;
+ }
+ else {
+ renderSeparator();
+ isFirst = false;
+
+ if (emptySlots) {
+ this._renderEmptySlots(emptySlots);
+ emptySlots = 0;
+ }
+
+ let shortVal = this.message.shortenValueGrip(item);
+ let elem = this.message._renderValueGrip(shortVal, { concise: true });
+ this.element.appendChild(elem);
+ }
+ }
+
+ if (emptySlots) {
+ renderSeparator();
+ this._renderEmptySlots(emptySlots, false);
+ }
+
+ let shown = items.length;
+ if (shown < preview.length) {
+ this._text(", ");
+
+ let n = preview.length - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderEmptySlots: function(aNumSlots, aAppendComma=true) {
+ let slotLabel = l10n.getStr("emptySlotLabel");
+ let slotText = PluralForm.get(aNumSlots, slotLabel);
+ this._text("<" + slotText.replace("#1", aNumSlots) + ">");
+ if (aAppendComma) {
+ this._text(", ");
+ }
+ },
+
+}); // Widgets.ObjectRenderers.byKind.ArrayLike
+
+/**
+ * The widget used for displaying MapLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "MapLike",
+
+ render: function()
+ {
+ let {preview} = this.objectActor;
+ let {entries} = preview;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!entries || this.options.concise) {
+ if (typeof preview.size == "number") {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.size));
+ this._text("]");
+ }
+ return;
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ for (let [key, value] of entries) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let keyElem = this.message._renderValueGrip(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+
+ // Strings are property names.
+ if (keyElem.classList && keyElem.classList.contains("console-string")) {
+ keyElem.classList.remove("console-string");
+ keyElem.classList.add("cm-property");
+ }
+
+ container.appendChild(keyElem);
+
+ this._text(": ");
+
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ if (typeof preview.size == "number" && shown < preview.size) {
+ this._text(", ");
+
+ let n = preview.size - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.MapLike
+
+/**
+ * The widget used for displaying objects with a URL.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithURL",
+
+ render: function()
+ {
+ this.element = this._renderElement(this.objectActor,
+ this.objectActor.preview.url);
+ },
+
+ _renderElement: function(objectActor, url)
+ {
+ let container = this.el("span.kind-" + objectActor.preview.kind);
+
+ this._anchor(objectActor.class, {
+ className: "cm-variable",
+ appendTo: container,
+ });
+
+ if (!VariablesView.isFalsy({ value: url })) {
+ this._text(" \u2192 ", container);
+ let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, {
+ onlyCropQuery: !this.options.concise
+ });
+ this._anchor(shortUrl, { href: url, appendTo: container });
+ }
+
+ return container;
+ },
+}); // Widgets.ObjectRenderers.byKind.ObjectWithURL
+
+/**
+ * The widget used for displaying objects with a string next to them.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithText",
+
+ render: function()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!this.options.concise) {
+ this._text(" ");
+ this.element.appendChild(this.el("span.console-string",
+ VariablesView.getString(preview.text)));
+ }
+ },
+});
+
+/**
+ * The widget used for displaying DOM event previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMEvent",
+
+ render: function()
+ {
+ let {preview} = this.objectActor;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(preview.type || this.objectActor.class,
+ { className: "cm-variable" });
+
+ if (this.options.concise) {
+ return;
+ }
+
+ if (preview.eventKind == "key" && preview.modifiers &&
+ preview.modifiers.length) {
+ this._text(" ");
+
+ let mods = 0;
+ for (let mod of preview.modifiers) {
+ if (mods > 0) {
+ this._text("-");
+ }
+ container.appendChild(this.el("span.cm-keyword", mod));
+ mods++;
+ }
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ if (preview.target) {
+ container.appendChild(this.el("span.cm-property", "target"));
+ this._text(": ");
+ let target = this.message._renderValueGrip(preview.target, { concise: true });
+ container.appendChild(target);
+ shown++;
+ }
+
+ for (let key of Object.keys(preview.properties || {})) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ let value = preview.properties[key];
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMEvent
+
+/**
+ * The widget used for displaying DOM node previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMNode",
+
+ canRender: function(objectActor) {
+ let {preview} = objectActor;
+ if (!preview) {
+ return false;
+ }
+
+ switch (preview.nodeType) {
+ case Ci.nsIDOMNode.DOCUMENT_NODE:
+ case Ci.nsIDOMNode.ATTRIBUTE_NODE:
+ case Ci.nsIDOMNode.TEXT_NODE:
+ case Ci.nsIDOMNode.COMMENT_NODE:
+ case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
+ case Ci.nsIDOMNode.ELEMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ render: function()
+ {
+ switch (this.objectActor.preview.nodeType) {
+ case Ci.nsIDOMNode.DOCUMENT_NODE:
+ this._renderDocumentNode();
+ break;
+ case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.attributeNode.kind-" + preview.kind);
+ let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
+ this.element.appendChild(attr);
+ break;
+ }
+ case Ci.nsIDOMNode.TEXT_NODE:
+ this._renderTextNode();
+ break;
+ case Ci.nsIDOMNode.COMMENT_NODE:
+ this._renderCommentNode();
+ break;
+ case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
+ this._renderDocumentFragmentNode();
+ break;
+ case Ci.nsIDOMNode.ELEMENT_NODE:
+ this._renderElementNode();
+ break;
+ default:
+ throw new Error("Unsupported nodeType: " + preview.nodeType);
+ }
+ },
+
+ _renderDocumentNode: function()
+ {
+ let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
+ this.element = fn.call(this, this.objectActor,
+ this.objectActor.preview.location);
+ this.element.classList.add("documentNode");
+ },
+
+ _renderAttributeNode: function(nodeName, nodeValue, addLink)
+ {
+ let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
+
+ let fragment = this.document.createDocumentFragment();
+ if (addLink) {
+ this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
+ } else {
+ fragment.appendChild(this.el("span.cm-attribute", nodeName));
+ }
+
+ this._text("=", fragment);
+ fragment.appendChild(this.el("span.console-string",
+ '"' + escapeHTML(value) + '"'));
+
+ return fragment;
+ },
+
+ _renderTextNode: function()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.textNode.kind-" + preview.kind);
+
+ this._anchor(preview.nodeName, { className: "cm-variable" });
+ this._text(" ");
+
+ let text = VariablesView.getString(preview.textContent);
+ this.element.appendChild(this.el("span.console-string", text));
+ },
+
+ _renderCommentNode: function()
+ {
+ let {preview} = this.objectActor;
+ let comment = "<!-- " + VariablesView.getString(preview.textContent, {
+ noStringQuotes: true,
+ }) + " -->";
+
+ this.element = this._anchor(comment, {
+ className: "kind-" + preview.kind + " commentNode cm-comment",
+ });
+ },
+
+ _renderDocumentFragmentNode: function()
+ {
+ let {preview} = this.objectActor;
+ let {childNodes} = preview;
+ let container = this.element = this.el("span.documentFragmentNode.kind-" +
+ preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!childNodes || this.options.concise) {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.childNodesLength));
+ this._text("]");
+ return;
+ }
+
+ this._text(" [ ");
+
+ let shown = 0;
+ for (let item of childNodes) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let elem = this.message._renderValueGrip(item, { concise: true });
+ container.appendChild(elem);
+ shown++;
+ }
+
+ if (shown < preview.childNodesLength) {
+ this._text(", ");
+
+ let n = preview.childNodesLength - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderElementNode: function()
+ {
+ let doc = this.document;
+ let {attributes, nodeName} = this.objectActor.preview;
+
+ this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");
+
+ let openTag = this.el("span.cm-tag");
+ openTag.textContent = "<";
+ this.element.appendChild(openTag);
+
+ let tagName = this._anchor(nodeName, {
+ className: "cm-tag",
+ appendTo: openTag
+ });
+
+ if (this.options.concise) {
+ if (attributes.id) {
+ tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
+ }
+ if (attributes.class) {
+ tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
+ }
+ } else {
+ for (let name of Object.keys(attributes)) {
+ let attr = this._renderAttributeNode(" " + name, attributes[name]);
+ this.element.appendChild(attr);
+ }
+ }
+
+ let closeTag = this.el("span.cm-tag");
+ closeTag.textContent = ">";
+ this.element.appendChild(closeTag);
+
+ // Register this widget in the owner message so that it gets destroyed when
+ // the message is destroyed.
+ this.message.widgets.add(this);
+
+ this.linkToInspector().then(null, Cu.reportError);
+ },
+
+ /**
+ * If the DOMNode being rendered can be highlit in the page, this function
+ * will attach mouseover/out event listeners to do so, and the inspector icon
+ * to open the node in the inspector.
+ * @return a promise that resolves when the node has been linked to the
+ * inspector, or rejects if it wasn't (either if no toolbox could be found to
+ * access the inspector, or if the node isn't present in the inspector, i.e.
+ * if the node is in a DocumentFragment or not part of the tree, or not of
+ * type Ci.nsIDOMNode.ELEMENT_NODE).
+ */
+ linkToInspector: Task.async(function*()
+ {
+ if (this._linkedToInspector) {
+ return;
+ }
+
+ // Checking the node type
+ if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+ throw new Error("The object cannot be linked to the inspector as it " +
+ "isn't an element node");
+ }
+
+ // Checking the presence of a toolbox
+ let target = this.message.output.toolboxTarget;
+ this.toolbox = gDevTools.getToolbox(target);
+ if (!this.toolbox) {
+ throw new Error("The object cannot be linked to the inspector without a " +
+ "toolbox");
+ }
+
+ // Checking that the inspector supports the node
+ yield this.toolbox.initInspector();
+ this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
+ if (!this._nodeFront) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "corresponding nodeFront could not be found");
+ }
+
+ // At this stage, the message may have been cleared already
+ if (!this.document) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "message was got cleared away");
+ }
+
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.element.addEventListener("mouseover", this.highlightDomNode, false);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+ this._openInspectorNode = this._anchor("", {
+ className: "open-inspector",
+ onClick: this.openNodeInInspector.bind(this)
+ });
+ this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
+
+ this._linkedToInspector = true;
+ }),
+
+ /**
+ * Highlight the DOMNode corresponding to the ObjectActor in the page.
+ * @return a promise that resolves when the node has been highlighted, or
+ * rejects if the node cannot be highlighted (detached from the DOM)
+ */
+ highlightDomNode: Task.async(function*()
+ {
+ yield this.linkToInspector();
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+ } else {
+ throw null;
+ }
+ }),
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ * @return a promise that resolves when the highlighter has been hidden
+ */
+ unhighlightDomNode: function()
+ {
+ return this.linkToInspector().then(() => {
+ return this.toolbox.highlighterUtils.unhighlight();
+ }).then(null, Cu.reportError);
+ },
+
+ /**
+ * Open the DOMNode corresponding to the ObjectActor in the inspector panel
+ * @return a promise that resolves when the inspector has been switched to
+ * and the node has been selected, or rejects if the node cannot be selected
+ * (detached from the DOM). Note that in any case, the inspector panel will
+ * be switched to.
+ */
+ openNodeInInspector: Task.async(function*()
+ {
+ yield this.linkToInspector();
+ yield this.toolbox.selectTool("inspector");
+
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ let onReady = this.toolbox.inspector.once("inspector-updated");
+ yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
+ yield onReady;
+ } else {
+ throw null;
+ }
+ }),
+
+ destroy: function()
+ {
+ if (this.toolbox && this._nodeFront) {
+ this.element.removeEventListener("mouseover", this.highlightDomNode, false);
+ this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
+ this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
+ this.toolbox = null;
+ this._nodeFront = null;
+ }
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMNode
+
+/**
+ * The widget user for displaying Promise objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Promise",
+
+ render: function()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview;
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ let container = this.element;
+ let addedPromiseInternalProps = false;
+
+ if (this.objectActor.promiseState) {
+ const { state, value, reason } = this.objectActor.promiseState;
+
+ this._renderObjectProperty("<state>", state, container, false);
+ addedPromiseInternalProps = true;
+
+ if (state == "fulfilled") {
+ this._renderObjectProperty("<value>", value, container, true);
+ } else if (state == "rejected") {
+ this._renderObjectProperty("<reason>", reason, container, true);
+ }
+ }
+
+ this._renderObjectProperties(container, addedPromiseInternalProps);
+ this._renderObjectSuffix();
+ }
+}); // Widgets.ObjectRenderers.byClass.Promise
+
+/**
+ * The widget used for displaying generic JS object previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "Object",
+
+ render: function()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview;
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ this._renderObjectProperties(this.element, false);
+ this._renderObjectSuffix();
+ },
+}); // Widgets.ObjectRenderers.byKind.Object
+
+/**
+ * The long string widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object longStringActor
+ * The LongStringActor to display.
+ */
+Widgets.LongString = function(message, longStringActor)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.longStringActor = longStringActor;
+ this._onClick = this._onClick.bind(this);
+ this._onSubstring = this._onSubstring.bind(this);
+};
+
+Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * The LongStringActor displayed by the widget.
+ * @type object
+ */
+ longStringActor: null,
+
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "span");
+ result.className = "longString console-string";
+ this._renderString(this.longStringActor.initial);
+ result.appendChild(this._renderEllipsis());
+
+ return this;
+ },
+
+ /**
+ * Render the long string in the widget element.
+ * @private
+ * @param string str
+ * The string to display.
+ */
+ _renderString: function(str)
+ {
+ this.element.textContent = VariablesView.getString(str, {
+ noStringQuotes: !this.message._quoteStrings,
+ noEllipsis: true,
+ });
+ },
+
+ /**
+ * Render the anchor ellipsis that allows the user to expand the long string.
+ *
+ * @private
+ * @return Element
+ */
+ _renderEllipsis: function()
+ {
+ let ellipsis = this.document.createElementNS(XHTML_NS, "a");
+ ellipsis.className = "longStringEllipsis";
+ ellipsis.textContent = l10n.getStr("longStringEllipsis");
+ ellipsis.href = "#";
+ ellipsis.draggable = false;
+ this.message._addLinkCallback(ellipsis, this._onClick);
+
+ return ellipsis;
+ },
+
+ /**
+ * The click event handler for the ellipsis shown after the short string. This
+ * function expands the element to show the full string.
+ * @private
+ */
+ _onClick: function()
+ {
+ let longString = this.output.webConsoleClient.longString(this.longStringActor);
+ let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
+
+ longString.substring(longString.initial.length, toIndex, this._onSubstring);
+ },
+
+ /**
+ * The longString substring response callback.
+ *
+ * @private
+ * @param object response
+ * Response packet.
+ */
+ _onSubstring: function(response)
+ {
+ if (response.error) {
+ Cu.reportError("LongString substring failure: " + response.error);
+ return;
+ }
+
+ this.element.lastChild.remove();
+ this.element.classList.remove("longString");
+
+ this._renderString(this.longStringActor.initial + response.substring);
+
+ this.output.owner.emit("new-messages", new Set([{
+ update: true,
+ node: this.message.element,
+ response: response,
+ }]));
+
+ let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
+ if (toIndex != this.longStringActor.length) {
+ this._logWarningAboutStringTooLong();
+ }
+ },
+
+ /**
+ * Inform user that the string he tries to view is too long.
+ * @private
+ */
+ _logWarningAboutStringTooLong: function()
+ {
+ let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
+ category: "output",
+ severity: "warning",
+ });
+ this.output.addMessage(msg);
+ },
+}); // Widgets.LongString.prototype
+
+
+/**
+ * The stacktrace widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array stacktrace
+ * The stacktrace to display, array of frames as supplied by the server,
+ * over the remote protocol.
+ */
+Widgets.Stacktrace = function(message, stacktrace)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.stacktrace = stacktrace;
+};
+
+Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * The stackframes received from the server.
+ * @type array
+ */
+ stacktrace: null,
+
+ render: function()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
+ result.className = "stacktrace devtools-monospace";
+
+ for (let frame of this.stacktrace) {
+ result.appendChild(this._renderFrame(frame));
+ }
+
+ return this;
+ },
+
+ /**
+ * Render a frame object received from the server.
+ *
+ * @param object frame
+ * The stack frame to display. This object should have the following
+ * properties: functionName, filename and lineNumber.
+ * @return DOMElement
+ * The DOM element to display for the given frame.
+ */
+ _renderFrame: function(frame)
+ {
+ let fn = this.document.createElementNS(XHTML_NS, "span");
+ fn.className = "function";
+ if (frame.functionName) {
+ let span = this.document.createElementNS(XHTML_NS, "span");
+ span.className = "cm-variable";
+ span.textContent = frame.functionName;
+ fn.appendChild(span);
+ fn.appendChild(this.document.createTextNode("()"));
+ } else {
+ fn.classList.add("cm-comment");
+ fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
+ }
+
+ let location = this.output.owner.createLocationNode({url: frame.filename,
+ line: frame.lineNumber},
+ "jsdebugger");
+
+ // .devtools-monospace sets font-size to 80%, however .body already has
+ // .devtools-monospace. If we keep it here, the location would be rendered
+ // smaller.
+ location.classList.remove("devtools-monospace");
+
+ let elem = this.document.createElementNS(XHTML_NS, "li");
+ elem.appendChild(fn);
+ elem.appendChild(location);
+ elem.appendChild(this.document.createTextNode("\n"));
+
+ return elem;
+ },
+}); // Widgets.Stacktrace.prototype
+
+
+/**
+ * The table widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array data
+ * Array of objects that holds the data to log in the table.
+ * @param object columns
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ */
+Widgets.Table = function(message, data, columns)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.data = data;
+ this.columns = columns;
+};
+
+Widgets.Table.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+ /**
+ * Array of objects that holds the data to output in the table.
+ * @type array
+ */
+ data: null,
+
+ /**
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ * @type object
+ */
+ columns: null,
+
+ render: function() {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "div");
+ result.className = "consoletable devtools-monospace";
+
+ this.table = new TableWidget(result, {
+ initialColumns: this.columns,
+ uniqueId: "_index",
+ firstColumn: "_index"
+ });
+
+ for (let row of this.data) {
+ this.table.push(row);
+ }
+
+ return this;
+ }
+}); // Widgets.Table.prototype
+
+function gSequenceId()
+{
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+exports.ConsoleOutput = ConsoleOutput;
+exports.Messages = Messages;
+exports.Widgets = Widgets;
diff --git a/toolkit/devtools/webconsole/hudservice.js b/toolkit/devtools/webconsole/hudservice.js
new file mode 100644
index 000000000..e8ef0fe65
--- /dev/null
+++ b/toolkit/devtools/webconsole/hudservice.js
@@ -0,0 +1,789 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+let Heritage = require("sdk/core/heritage");
+
+loader.lazyGetter(this, "Telemetry", () => require("devtools/shared/telemetry"));
+loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame);
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
+loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
+loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
+loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
+loader.lazyGetter(this, "showDoorhanger", () => require("devtools/shared/doorhanger").showDoorhanger);
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+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.";
+
+let 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,
+
+ /**
+ * Firefox-specific current tab getter
+ *
+ * @returns nsIDOMWindow
+ */
+ currentContext: function HS_currentContext() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ },
+
+ /**
+ * 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 || !devtools.TargetFactory.isKnownTab(tab)) {
+ return null;
+ }
+ let target = devtools.TargetFactory.forTab(tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let panel = toolbox ? toolbox.getPanel("webconsole") : null;
+ return panel ? panel.hud : null;
+ },
+
+ /**
+ * Toggle the Browser Console.
+ */
+ toggleBrowserConsole: function HS_toggleBrowserConsole()
+ {
+ if (this._browserConsoleID) {
+ let hud = this.getHudReferenceById(this._browserConsoleID);
+ return hud.destroy();
+ }
+
+ if (this._browserConsoleDefer) {
+ return this._browserConsoleDefer.promise;
+ }
+
+ this._browserConsoleDefer = promise.defer();
+
+ function connect()
+ {
+ let deferred = promise.defer();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ client.connect(() =>
+ client.listTabs((aResponse) => {
+ // Add Global Process debugging...
+ let globals = Cu.cloneInto(aResponse, {});
+ delete globals.tabs;
+ delete globals.selected;
+ // ...only if there are appropriate actors (a 'from' property will
+ // always be there).
+ if (Object.keys(globals).length > 1) {
+ deferred.resolve({ form: globals, client: client, chrome: true });
+ } else {
+ deferred.reject("Global console not found!");
+ }
+ }));
+
+ return deferred.promise;
+ }
+
+ let target;
+ function getTarget(aConnection)
+ {
+ let options = {
+ form: aConnection.form,
+ client: aConnection.client,
+ chrome: true,
+ };
+
+ return devtools.TargetFactory.forRemoteTab(options);
+ }
+
+ function openWindow(aTarget)
+ {
+ target = aTarget;
+
+ let deferred = promise.defer();
+
+ let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank",
+ BROWSER_CONSOLE_WINDOW_FEATURES, null);
+ win.addEventListener("DOMContentLoaded", function onLoad() {
+ win.removeEventListener("DOMContentLoaded", onLoad);
+
+ // Set the correct Browser Console title.
+ let root = win.document.documentElement;
+ root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
+
+ deferred.resolve(win);
+ });
+
+ return deferred.promise;
+ }
+
+ connect().then(getTarget).then(openWindow).then((aWindow) => {
+ this.openBrowserConsole(target, aWindow, aWindow)
+ .then((aBrowserConsole) => {
+ this._browserConsoleDefer.resolve(aBrowserConsole);
+ this._browserConsoleDefer = null;
+ })
+ }, console.error);
+
+ return this._browserConsoleDefer.promise;
+ },
+
+ /**
+ * Opens or focuses the Browser Console.
+ */
+ openBrowserConsoleOrFocus: function HS_openBrowserConsoleOrFocus()
+ {
+ let hud = this.getBrowserConsole();
+ if (hud) {
+ hud.iframeWindow.focus();
+ return promise.resolve(hud);
+ }
+ else {
+ return this.toggleBrowserConsole();
+ }
+ },
+
+ /**
+ * Get the Browser Console instance, if open.
+ *
+ * @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") != "navigator:browser") {
+ 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() 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)
+ {
+ this.gViewSourceUtils.viewSource(aSourceURL, null,
+ this.iframeWindow.document, aSourceLine);
+ },
+
+ /**
+ * 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.
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ * TODO: This function breaks the client-server boundaries.
+ * To be fixed in bug 793259.
+ */
+ viewSourceInStyleEditor:
+ function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+
+ gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
+ try {
+ toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
+ } catch(e) {
+ // Open view source if style editor fails.
+ this.viewSource(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.
+ *
+ * @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;
+ }
+
+ let showSource = ({ DebuggerView }) => {
+ let item = DebuggerView.Sources.getItemForAttachment(
+ a => a.source.url === aSourceURL
+ );
+ if (item) {
+ DebuggerView.setEditorLocation(item.attachment.source.actor, aSourceLine,
+ { noDebug: true }).then(() => {
+ this.ui.emit("source-in-debugger-opened");
+ });
+ return;
+ }
+ toolbox.selectTool("webconsole")
+ .then(() => this.viewSource(aSourceURL, aSourceLine));
+ }
+
+ // If the Debugger was already open, switch to it and try to show the
+ // source immediately. Otherwise, initialize it and wait for the sources
+ // to be added first.
+ let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+ toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
+ if (debuggerAlreadyOpen) {
+ showSource(dbg);
+ } else {
+ dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
+ }
+ });
+ },
+
+
+ /**
+ * Tries to open a JavaScript file related to the web page for the web console
+ * instance in the corresponding Scratchpad.
+ *
+ * @param string aSourceURL
+ * The URL of the file which corresponds to a Scratchpad id.
+ */
+ viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL)
+ {
+ // Check for matching top level Scratchpad window.
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+
+ if (!win.closed && win.Scratchpad.uniqueName === aSourceURL) {
+ win.focus();
+ return;
+ }
+ }
+
+ // Check for matching Scratchpad toolbox tab.
+ for (let [, toolbox] of gDevTools) {
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ if (scratchpadPanel) {
+ let { scratchpad } = scratchpadPanel;
+ if (scratchpad.uniqueName === aSourceURL) {
+ toolbox.selectTool("scratchpad");
+ toolbox.raise();
+ scratchpad.editor.focus();
+ return;
+ }
+ }
+ }
+ },
+
+ /**
+ * 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;
+ }
+ let framesController = panel.panelWin.DebuggerController.StackFrames;
+ let thread = framesController.activeThread;
+ if (thread && thread.paused) {
+ return {
+ frames: thread.cachedFrames,
+ selected: framesController.currentFrameDepth,
+ };
+ }
+ return null;
+ },
+
+ /**
+ * Retrieves the current selection from the Inspector, if such a selection
+ * exists. This is used to pass the ID of the selected actor to the Web
+ * Console server for the $0 helper.
+ *
+ * @return object|null
+ * A Selection referring to the currently selected node in the
+ * Inspector.
+ * If the inspector was never opened, or no node was ever selected,
+ * then |null| is returned.
+ */
+ getInspectorSelection: function WC_getInspectorSelection()
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ return null;
+ }
+ let panel = toolbox.getPanel("inspector");
+ if (!panel || !panel.selection) {
+ return null;
+ }
+ return panel.selection;
+ },
+
+ /**
+ * Destroy the object. Call this method to avoid memory leaks when the Web
+ * Console is closed.
+ *
+ * @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();
+
+ let popupset = this.mainPopupSet;
+ let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
+ for (let panel of panels) {
+ panel.hidePopup();
+ }
+
+ let onDestroy = function WC_onDestroyUI() {
+ try {
+ let tabWindow = this.target.isLocalTab ? this.target.window : null;
+ tabWindow && tabWindow.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 = Heritage.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);
+
+ // Make sure Ctrl-W closes the Browser Console window.
+ window.document.getElementById("cmd_close").removeAttribute("disabled");
+
+ this._telemetry.toolOpened("browserconsole");
+
+ // Create an onFocus handler just to display the dev edition promo.
+ // This is to prevent race conditions in some environments.
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ let onFocus = () => showDoorhanger({ window, type: "deveditionpromo" });
+ window.addEventListener("focus", onFocus);
+
+ this._bc_init = this.$init();
+ return this._bc_init;
+ },
+
+ $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(() => {
+ 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/toolkit/devtools/webconsole/moz.build b/toolkit/devtools/webconsole/moz.build
index 621a69399..fd2623bdc 100644
--- a/toolkit/devtools/webconsole/moz.build
+++ b/toolkit/devtools/webconsole/moz.build
@@ -4,13 +4,19 @@
# 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/.
-if CONFIG['OS_TARGET'] != 'Android':
- MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
- XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
-
EXTRA_JS_MODULES.devtools.toolkit.webconsole += [
'client.js',
'network-helper.js',
'network-monitor.js',
'utils.js',
]
+
+if CONFIG['MOZ_DEVTOOLS']:
+ EXTRA_JS_MODULES.devtools.webconsole += [
+ 'console-commands.js',
+ 'console-output.js',
+ 'hudservice.js',
+ 'network-panel.js',
+ 'panel.js',
+ 'webconsole.js',
+ ]
diff --git a/toolkit/devtools/webconsole/network-panel.js b/toolkit/devtools/webconsole/network-panel.js
new file mode 100644
index 000000000..626241b1d
--- /dev/null
+++ b/toolkit/devtools/webconsole/network-panel.js
@@ -0,0 +1,836 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService");
+
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+
+/**
+ * Creates a new NetworkPanel.
+ *
+ * @constructor
+ * @param nsIDOMNode aParent
+ * Parent node to append the created panel to.
+ * @param object aHttpActivity
+ * HttpActivity to display in the panel.
+ * @param object aWebConsoleFrame
+ * The parent WebConsoleFrame object that owns this network panel
+ * instance.
+ */
+function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame)
+{
+ let doc = aParent.ownerDocument;
+ this.httpActivity = aHttpActivity;
+ this.webconsole = aWebConsoleFrame;
+ this._longStringClick = this._longStringClick.bind(this);
+ this._responseBodyFetch = this._responseBodyFetch.bind(this);
+ this._requestBodyFetch = this._requestBodyFetch.bind(this);
+
+ // Create the underlaying panel
+ this.panel = createElement(doc, "panel", {
+ label: l10n.getStr("NetworkPanel.label"),
+ titlebar: "normal",
+ noautofocus: "true",
+ noautohide: "true",
+ close: "true"
+ });
+
+ // Create the iframe that displays the NetworkPanel XHTML.
+ this.iframe = createAndAppendElement(this.panel, "iframe", {
+ src: "chrome://browser/content/devtools/NetworkPanel.xhtml",
+ type: "content",
+ flex: "1"
+ });
+
+ let self = this;
+
+ // Destroy the panel when it's closed.
+ this.panel.addEventListener("popuphidden", function onPopupHide() {
+ self.panel.removeEventListener("popuphidden", onPopupHide, false);
+ self.panel.parentNode.removeChild(self.panel);
+ self.panel = null;
+ self.iframe = null;
+ self.httpActivity = null;
+ self.webconsole = null;
+
+ if (self.linkNode) {
+ self.linkNode._panelOpen = false;
+ self.linkNode = null;
+ }
+ }, false);
+
+ // Set the document object and update the content once the panel is loaded.
+ this.iframe.addEventListener("load", function onLoad() {
+ if (!self.iframe) {
+ return;
+ }
+
+ self.iframe.removeEventListener("load", onLoad, true);
+ self.update();
+ }, true);
+
+ this.panel.addEventListener("popupshown", function onPopupShown() {
+ self.panel.removeEventListener("popupshown", onPopupShown, true);
+ self.update();
+ }, true);
+
+ // Create the footer.
+ let footer = createElement(doc, "hbox", { align: "end" });
+ createAndAppendElement(footer, "spacer", { flex: 1 });
+
+ createAndAppendElement(footer, "resizer", { dir: "bottomend" });
+ this.panel.appendChild(footer);
+
+ aParent.appendChild(this.panel);
+}
+exports.NetworkPanel = NetworkPanel;
+
+NetworkPanel.prototype =
+{
+ /**
+ * The current state of the output.
+ */
+ _state: 0,
+
+ /**
+ * State variables.
+ */
+ _INIT: 0,
+ _DISPLAYED_REQUEST_HEADER: 1,
+ _DISPLAYED_REQUEST_BODY: 2,
+ _DISPLAYED_RESPONSE_HEADER: 3,
+ _TRANSITION_CLOSED: 4,
+
+ _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,
+
+ _contentType: null,
+
+ /**
+ * Function callback invoked whenever the panel content is updated. This is
+ * used only by tests.
+ *
+ * @private
+ * @type function
+ */
+ _onUpdate: null,
+
+ get document() {
+ return this.iframe && this.iframe.contentWindow ?
+ this.iframe.contentWindow.document : null;
+ },
+
+ /**
+ * Small helper function that is nearly equal to l10n.getFormatStr
+ * except that it prefixes aName with "NetworkPanel.".
+ *
+ * @param string aName
+ * The name of an i10n string to format. This string is prefixed with
+ * "NetworkPanel." before calling the HUDService.getFormatStr function.
+ * @param array aArray
+ * Values used as placeholder for the i10n string.
+ * @returns string
+ * The i10n formated string.
+ */
+ _format: function NP_format(aName, aArray)
+ {
+ return l10n.getFormatStr("NetworkPanel." + aName, aArray);
+ },
+
+ /**
+ * Returns the content type of the response body. This is based on the
+ * response.content.mimeType property. If this value is not available, then
+ * the content type is guessed by the file extension of the request URL.
+ *
+ * @return string
+ * Content type or empty string if no content type could be figured
+ * out.
+ */
+ get contentType()
+ {
+ if (this._contentType) {
+ return this._contentType;
+ }
+
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+
+ let contentType = "";
+ let types = response.content ?
+ (response.content.mimeType || "").split(/,|;/) : [];
+ for (let i = 0; i < types.length; i++) {
+ if (types[i] in NetworkHelper.mimeCategoryMap) {
+ contentType = types[i];
+ break;
+ }
+ }
+
+ if (contentType) {
+ this._contentType = contentType;
+ return contentType;
+ }
+
+ // Try to get the content type from the request file extension.
+ let uri = NetUtil.newURI(request.url);
+ if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
+ try {
+ contentType = mimeService.getTypeFromExtension(uri.fileExtension);
+ }
+ catch(ex) {
+ // Added to prevent failures on OS X 64. No Flash?
+ Cu.reportError(ex);
+ }
+ }
+
+ this._contentType = contentType;
+ return contentType;
+ },
+
+ /**
+ *
+ * @returns boolean
+ * True if the response is an image, false otherwise.
+ */
+ get _responseIsImage()
+ {
+ return this.contentType &&
+ NetworkHelper.mimeCategoryMap[this.contentType] == "image";
+ },
+
+ /**
+ *
+ * @returns boolean
+ * True if the response body contains text, false otherwise.
+ */
+ get _isResponseBodyTextData()
+ {
+ return this.contentType ?
+ NetworkHelper.isTextMimeType(this.contentType) : false;
+ },
+
+ /**
+ * Tells if the server response is cached.
+ *
+ * @returns boolean
+ * Returns true if the server responded that the request is already
+ * in the browser's cache, false otherwise.
+ */
+ get _isResponseCached()
+ {
+ return this.httpActivity.response.status == 304;
+ },
+
+ /**
+ * Tells if the request body includes form data.
+ *
+ * @returns boolean
+ * Returns true if the posted body contains form data.
+ */
+ get _isRequestBodyFormData()
+ {
+ let requestBody = this.httpActivity.request.postData.text;
+ if (typeof requestBody == "object" && requestBody.type == "longString") {
+ requestBody = requestBody.initial;
+ }
+ return this._fromDataRegExp.test(requestBody);
+ },
+
+ /**
+ * Appends the node with id=aId by the text aValue.
+ *
+ * @private
+ * @param string aId
+ * @param string aValue
+ * @return nsIDOMElement
+ * The DOM element with id=aId.
+ */
+ _appendTextNode: function NP__appendTextNode(aId, aValue)
+ {
+ let textNode = this.document.createTextNode(aValue);
+ let elem = this.document.getElementById(aId);
+ elem.appendChild(textNode);
+ return elem;
+ },
+
+ /**
+ * Generates some HTML to display the key-value pair of the aList data. The
+ * generated HTML is added to node with id=aParentId.
+ *
+ * @param string aParentId
+ * Id of the parent node to append the list to.
+ * @oaram array aList
+ * Array that holds the objects you want to display. Each object must
+ * have two properties: name and value.
+ * @param boolean aIgnoreCookie
+ * If true, the key-value named "Cookie" is not added to the list.
+ * @returns void
+ */
+ _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
+ {
+ let parent = this.document.getElementById(aParentId);
+ let doc = this.document;
+
+ aList.sort(function(a, b) {
+ return a.name.toLowerCase() < b.name.toLowerCase();
+ });
+
+ aList.forEach((aItem) => {
+ let name = aItem.name;
+ if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) {
+ return;
+ }
+
+ let value = aItem.value;
+ let longString = null;
+ if (typeof value == "object" && value.type == "longString") {
+ value = value.initial;
+ longString = true;
+ }
+
+ /**
+ * The following code creates the HTML:
+ * <tr>
+ * <th scope="row" class="property-name">${line}:</th>
+ * <td class="property-value">${aList[line]}</td>
+ * </tr>
+ * and adds it to parent.
+ */
+ let row = doc.createElement("tr");
+ let textNode = doc.createTextNode(name + ":");
+ let th = doc.createElement("th");
+ th.setAttribute("scope", "row");
+ th.setAttribute("class", "property-name");
+ th.appendChild(textNode);
+ row.appendChild(th);
+
+ textNode = doc.createTextNode(value);
+ let td = doc.createElement("td");
+ td.setAttribute("class", "property-value");
+ td.appendChild(textNode);
+
+ if (longString) {
+ let a = doc.createElement("a");
+ a.href = "#";
+ a.className = "longStringEllipsis";
+ a.addEventListener("mousedown", this._longStringClick.bind(this, aItem));
+ a.textContent = l10n.getStr("longStringEllipsis");
+ td.appendChild(a);
+ }
+
+ row.appendChild(td);
+
+ parent.appendChild(row);
+ });
+ },
+
+ /**
+ * The click event handler for the ellipsis which allows the user to retrieve
+ * the full header value.
+ *
+ * @private
+ * @param object aHeader
+ * The header object with the |name| and |value| properties.
+ * @param nsIDOMEvent aEvent
+ * The DOM click event object.
+ */
+ _longStringClick: function NP__longStringClick(aHeader, aEvent)
+ {
+ aEvent.preventDefault();
+
+ let longString = this.webconsole.webConsoleClient.longString(aHeader.value);
+
+ longString.substring(longString.initial.length, longString.length,
+ function NP__onLongStringSubstring(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ aHeader.value = aHeader.value.initial + aResponse.substring;
+
+ let textNode = aEvent.target.previousSibling;
+ textNode.textContent += aResponse.substring;
+ textNode.parentNode.removeChild(aEvent.target);
+ });
+ },
+
+ /**
+ * Displays the node with id=aId.
+ *
+ * @private
+ * @param string aId
+ * @return nsIDOMElement
+ * The element with id=aId.
+ */
+ _displayNode: function NP__displayNode(aId)
+ {
+ let elem = this.document.getElementById(aId);
+ elem.style.display = "block";
+ },
+
+ /**
+ * Sets the request URL, request method, the timing information when the
+ * request started and the request header content on the NetworkPanel.
+ * If the request header contains cookie data, a list of sent cookies is
+ * generated and a special sent cookie section is displayed + the cookie list
+ * added to it.
+ *
+ * @returns void
+ */
+ _displayRequestHeader: function NP__displayRequestHeader()
+ {
+ let request = this.httpActivity.request;
+ let requestTime = new Date(this.httpActivity.startedDateTime);
+
+ this._appendTextNode("headUrl", request.url);
+ this._appendTextNode("headMethod", request.method);
+ this._appendTextNode("requestHeadersInfo",
+ l10n.timestampString(requestTime));
+
+ this._appendList("requestHeadersContent", request.headers, true);
+
+ if (request.cookies.length > 0) {
+ this._displayNode("requestCookie");
+ this._appendList("requestCookieContent", request.cookies);
+ }
+ },
+
+ /**
+ * Displays the request body section of the NetworkPanel and set the request
+ * body content on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayRequestBody: function NP__displayRequestBody()
+ {
+ let postData = this.httpActivity.request.postData;
+ this._displayNode("requestBody");
+ this._appendTextNode("requestBodyContent", postData.text);
+ },
+
+ /*
+ * Displays the `sent form data` section. Parses the request header for the
+ * submitted form data displays it inside of the `sent form data` section.
+ *
+ * @returns void
+ */
+ _displayRequestForm: function NP__processRequestForm()
+ {
+ let postData = this.httpActivity.request.postData.text;
+ let requestBodyLines = postData.split("\n");
+ let formData = requestBodyLines[requestBodyLines.length - 1].
+ replace(/\+/g, " ").split("&");
+
+ function unescapeText(aText)
+ {
+ try {
+ return decodeURIComponent(aText);
+ }
+ catch (ex) {
+ return decodeURIComponent(unescape(aText));
+ }
+ }
+
+ let formDataArray = [];
+ for (let i = 0; i < formData.length; i++) {
+ let data = formData[i];
+ let idx = data.indexOf("=");
+ let key = data.substring(0, idx);
+ let value = data.substring(idx + 1);
+ formDataArray.push({
+ name: unescapeText(key),
+ value: unescapeText(value)
+ });
+ }
+
+ this._appendList("requestFormDataContent", formDataArray);
+ this._displayNode("requestFormData");
+ },
+
+ /**
+ * Displays the response section of the NetworkPanel, sets the response status,
+ * the duration between the start of the request and the receiving of the
+ * response header as well as the response header content on the the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseHeader: function NP__displayResponseHeader()
+ {
+ let timing = this.httpActivity.timings;
+ let response = this.httpActivity.response;
+
+ this._appendTextNode("headStatus",
+ [response.httpVersion, response.status,
+ response.statusText].join(" "));
+
+ // Calculate how much time it took from the request start, until the
+ // response started to be received.
+ let deltaDuration = 0;
+ ["dns", "connect", "send", "wait"].forEach(function (aValue) {
+ let ms = timing[aValue];
+ if (ms > -1) {
+ deltaDuration += ms;
+ }
+ });
+
+ this._appendTextNode("responseHeadersInfo",
+ this._format("durationMS", [deltaDuration]));
+
+ this._displayNode("responseContainer");
+ this._appendList("responseHeadersContent", response.headers, true);
+
+ if (response.cookies.length > 0) {
+ this._displayNode("responseCookie");
+ this._appendList("responseCookieContent", response.cookies);
+ }
+ },
+
+ /**
+ * Displays the respones image section, sets the source of the image displayed
+ * in the image response section to the request URL and the duration between
+ * the receiving of the response header and the end of the request. Once the
+ * image is loaded, the size of the requested image is set.
+ *
+ * @returns void
+ */
+ _displayResponseImage: function NP__displayResponseImage()
+ {
+ let self = this;
+ let timing = this.httpActivity.timings;
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+ let cached = "";
+
+ if (this._isResponseCached) {
+ cached = "Cached";
+ }
+
+ let imageNode = this.document.getElementById("responseImage" +
+ cached + "Node");
+
+ let text = response.content.text;
+ if (typeof text == "object" && text.type == "longString") {
+ this._showResponseBodyFetchLink();
+ }
+ else {
+ imageNode.setAttribute("src",
+ "data:" + this.contentType + ";base64," + text);
+ }
+
+ // This function is called to set the imageInfo.
+ function setImageInfo() {
+ self._appendTextNode("responseImage" + cached + "Info",
+ self._format("imageSizeDeltaDurationMS",
+ [ imageNode.width, imageNode.height, timing.receive ]
+ )
+ );
+ }
+
+ // Check if the image is already loaded.
+ if (imageNode.width != 0) {
+ setImageInfo();
+ }
+ else {
+ // Image is not loaded yet therefore add a load event.
+ imageNode.addEventListener("load", function imageNodeLoad() {
+ imageNode.removeEventListener("load", imageNodeLoad, false);
+ setImageInfo();
+ }, false);
+ }
+
+ this._displayNode("responseImage" + cached);
+ },
+
+ /**
+ * Displays the response body section, sets the the duration between
+ * the receiving of the response header and the end of the request as well as
+ * the content of the response body on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseBody: function NP__displayResponseBody()
+ {
+ let timing = this.httpActivity.timings;
+ let response = this.httpActivity.response;
+ let cached = this._isResponseCached ? "Cached" : "";
+
+ this._appendTextNode("responseBody" + cached + "Info",
+ this._format("durationMS", [timing.receive]));
+
+ this._displayNode("responseBody" + cached);
+
+ let text = response.content.text;
+ if (typeof text == "object") {
+ text = text.initial;
+ this._showResponseBodyFetchLink();
+ }
+
+ this._appendTextNode("responseBody" + cached + "Content", text);
+ },
+
+ /**
+ * Show the "fetch response body" link.
+ * @private
+ */
+ _showResponseBodyFetchLink: function NP__showResponseBodyFetchLink()
+ {
+ let content = this.httpActivity.response.content;
+
+ let elem = this._appendTextNode("responseBodyFetchLink",
+ this._format("fetchRemainingResponseContentLink",
+ [content.text.length - content.text.initial.length]));
+
+ elem.style.display = "block";
+ elem.addEventListener("mousedown", this._responseBodyFetch);
+ },
+
+ /**
+ * Click event handler for the link that allows users to fetch the remaining
+ * response body.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _responseBodyFetch: function NP__responseBodyFetch(aEvent)
+ {
+ aEvent.target.style.display = "none";
+ aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
+
+ let content = this.httpActivity.response.content;
+ let longString = this.webconsole.webConsoleClient.longString(content.text);
+ longString.substring(longString.initial.length, longString.length,
+ (aResponse) =>
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ content.text = content.text.initial + aResponse.substring;
+ let cached = this._isResponseCached ? "Cached" : "";
+
+ if (this._responseIsImage) {
+ let imageNode = this.document.getElementById("responseImage" +
+ cached + "Node");
+ imageNode.src =
+ "data:" + this.contentType + ";base64," + content.text;
+ }
+ else {
+ this._appendTextNode("responseBody" + cached + "Content",
+ aResponse.substring);
+ }
+ });
+ },
+
+ /**
+ * Displays the `Unknown Content-Type hint` and sets the duration between the
+ * receiving of the response header on the NetworkPanel.
+ *
+ * @returns void
+ */
+ _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType()
+ {
+ let timing = this.httpActivity.timings;
+
+ this._displayNode("responseBodyUnknownType");
+ this._appendTextNode("responseBodyUnknownTypeInfo",
+ this._format("durationMS", [timing.receive]));
+
+ this._appendTextNode("responseBodyUnknownTypeContent",
+ this._format("responseBodyUnableToDisplay.content", [this.contentType]));
+ },
+
+ /**
+ * Displays the `no response body` section and sets the the duration between
+ * the receiving of the response header and the end of the request.
+ *
+ * @returns void
+ */
+ _displayNoResponseBody: function NP_displayNoResponseBody()
+ {
+ let timing = this.httpActivity.timings;
+
+ this._displayNode("responseNoBody");
+ this._appendTextNode("responseNoBodyInfo",
+ this._format("durationMS", [timing.receive]));
+ },
+
+ /**
+ * Updates the content of the NetworkPanel's iframe.
+ *
+ * @returns void
+ */
+ update: function NP_update()
+ {
+ if (!this.document || this.document.readyState != "complete") {
+ return;
+ }
+
+ let updates = this.httpActivity.updates;
+ let timing = this.httpActivity.timings;
+ let request = this.httpActivity.request;
+ let response = this.httpActivity.response;
+
+ switch (this._state) {
+ case this._INIT:
+ this._displayRequestHeader();
+ this._state = this._DISPLAYED_REQUEST_HEADER;
+ // FALL THROUGH
+
+ case this._DISPLAYED_REQUEST_HEADER:
+ // Process the request body if there is one.
+ if (!this.httpActivity.discardRequestBody && request.postData.text) {
+ this._updateRequestBody();
+ this._state = this._DISPLAYED_REQUEST_BODY;
+ }
+ // FALL THROUGH
+
+ case this._DISPLAYED_REQUEST_BODY:
+ if (!response.headers.length || !Object.keys(timing).length) {
+ break;
+ }
+ this._displayResponseHeader();
+ this._state = this._DISPLAYED_RESPONSE_HEADER;
+ // FALL THROUGH
+
+ case this._DISPLAYED_RESPONSE_HEADER:
+ if (updates.indexOf("responseContent") == -1 ||
+ updates.indexOf("eventTimings") == -1) {
+ break;
+ }
+
+ this._state = this._TRANSITION_CLOSED;
+ if (this.httpActivity.discardResponseBody) {
+ break;
+ }
+
+ if (!response.content || !response.content.text) {
+ this._displayNoResponseBody();
+ }
+ else if (this._responseIsImage) {
+ this._displayResponseImage();
+ }
+ else if (!this._isResponseBodyTextData) {
+ this._displayResponseBodyUnknownType();
+ }
+ else if (response.content.text) {
+ this._displayResponseBody();
+ }
+ break;
+ }
+
+ if (this._onUpdate) {
+ this._onUpdate();
+ }
+ },
+
+ /**
+ * Update the panel to hold the current information we have about the request
+ * body.
+ * @private
+ */
+ _updateRequestBody: function NP__updateRequestBody()
+ {
+ let postData = this.httpActivity.request.postData;
+ if (typeof postData.text == "object" && postData.text.type == "longString") {
+ let elem = this._appendTextNode("requestBodyFetchLink",
+ this._format("fetchRemainingRequestContentLink",
+ [postData.text.length - postData.text.initial.length]));
+
+ elem.style.display = "block";
+ elem.addEventListener("mousedown", this._requestBodyFetch);
+ return;
+ }
+
+ // Check if we send some form data. If so, display the form data special.
+ if (this._isRequestBodyFormData) {
+ this._displayRequestForm();
+ }
+ else {
+ this._displayRequestBody();
+ }
+ },
+
+ /**
+ * Click event handler for the link that allows users to fetch the remaining
+ * request body.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _requestBodyFetch: function NP__requestBodyFetch(aEvent)
+ {
+ aEvent.target.style.display = "none";
+ aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
+
+ let postData = this.httpActivity.request.postData;
+ let longString = this.webconsole.webConsoleClient.longString(postData.text);
+ longString.substring(longString.initial.length, longString.length,
+ (aResponse) =>
+ {
+ if (aResponse.error) {
+ Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
+ return;
+ }
+
+ postData.text = postData.text.initial + aResponse.substring;
+ this._updateRequestBody();
+ });
+ },
+};
+
+/**
+ * Creates a DOMNode and sets all the attributes of aAttributes on the created
+ * element.
+ *
+ * @param nsIDOMDocument aDocument
+ * Document to create the new DOMNode.
+ * @param string aTag
+ * Name of the tag for the DOMNode.
+ * @param object aAttributes
+ * Attributes set on the created DOMNode.
+ *
+ * @returns nsIDOMNode
+ */
+function createElement(aDocument, aTag, aAttributes)
+{
+ let node = aDocument.createElement(aTag);
+ if (aAttributes) {
+ for (let attr in aAttributes) {
+ node.setAttribute(attr, aAttributes[attr]);
+ }
+ }
+ return node;
+}
+
+/**
+ * Creates a new DOMNode and appends it to aParent.
+ *
+ * @param nsIDOMNode aParent
+ * A parent node to append the created element.
+ * @param string aTag
+ * Name of the tag for the DOMNode.
+ * @param object aAttributes
+ * Attributes set on the created DOMNode.
+ *
+ * @returns nsIDOMNode
+ */
+function createAndAppendElement(aParent, aTag, aAttributes)
+{
+ let node = createElement(aParent.ownerDocument, aTag, aAttributes);
+ aParent.appendChild(node);
+ return node;
+}
diff --git a/toolkit/devtools/webconsole/panel.js b/toolkit/devtools/webconsole/panel.js
new file mode 100644
index 000000000..f5c0e0cfd
--- /dev/null
+++ b/toolkit/devtools/webconsole/panel.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
+loader.lazyGetter(this, "HUDService", () => require("devtools/webconsole/hudservice"));
+loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+
+/**
+ * A DevToolPanel that controls the Web Console.
+ */
+function WebConsolePanel(iframeWindow, toolbox)
+{
+ this._frameWindow = iframeWindow;
+ this._toolbox = toolbox;
+ EventEmitter.decorate(this);
+}
+
+exports.WebConsolePanel = WebConsolePanel;
+
+WebConsolePanel.prototype = {
+ hud: null,
+
+ /**
+ * Called by the WebConsole's onkey command handler.
+ * If the WebConsole is opened, check if the JSTerm's input line has focus.
+ * If not, focus it.
+ */
+ focusInput: function WCP_focusInput()
+ {
+ let inputNode = this.hud.jsterm.inputNode;
+
+ if (!inputNode.getAttribute("focused"))
+ {
+ inputNode.focus();
+ }
+ },
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Web Console completes opening.
+ */
+ open: function WCP_open()
+ {
+ 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((aTarget) => {
+ this._frameWindow._remoteTarget = aTarget;
+
+ let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
+ let chromeWindow = iframe.ownerDocument.defaultView;
+ return HUDService.openWebConsole(this.target, webConsoleUIWindow,
+ chromeWindow);
+ })
+ .then((aWebConsole) => {
+ this.hud = aWebConsole;
+ this._isReady = true;
+ this.emit("ready");
+ return this;
+ }, (aReason) => {
+ let msg = "WebConsolePanel open failed. " +
+ aReason.error + ": " + aReason.message;
+ dump(msg + "\n");
+ Cu.reportError(msg);
+ });
+ },
+
+ get target() this._toolbox.target,
+
+ _isReady: false,
+ get isReady() this._isReady,
+
+ destroy: function WCP_destroy()
+ {
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ this._destroyer = this.hud.destroy();
+ this._destroyer.then(() => this.emit("destroyed"));
+
+ return this._destroyer;
+ },
+};
diff --git a/toolkit/devtools/webconsole/test/chrome.ini b/toolkit/devtools/webconsole/test/chrome.ini
deleted file mode 100644
index b8558bb27..000000000
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ /dev/null
@@ -1,28 +0,0 @@
-[DEFAULT]
-support-files =
- common.js
- data.json
- data.json^headers^
- network_requests_iframe.html
- sandboxed_iframe.html
-
-[test_basics.html]
-[test_bug819670_getter_throws.html]
-[test_cached_messages.html]
-[test_consoleapi.html]
-[test_consoleapi_innerID.html]
-[test_file_uri.html]
-[test_reflow.html]
-[test_jsterm.html]
-[test_network_get.html]
-[test_network_longstring.html]
-[test_network_post.html]
-[test_network_security-hpkp.html]
-[test_network_security-hsts.html]
-[test_nsiconsolemessage.html]
-[test_object_actor.html]
-[test_object_actor_native_getters.html]
-[test_object_actor_native_getters_lenient_this.html]
-[test_page_errors.html]
-[test_throw.html]
-[test_jsterm_cd_iframe.html]
diff --git a/toolkit/devtools/webconsole/test/common.js b/toolkit/devtools/webconsole/test/common.js
deleted file mode 100644
index 62fc6ad37..000000000
--- a/toolkit/devtools/webconsole/test/common.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-
-Cu.import("resource://gre/modules/Services.jsm");
-
-let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
-let WebConsoleUtils = devtools.require("devtools/toolkit/webconsole/utils").Utils;
-
-let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
- .getService(Ci.nsIConsoleAPIStorage);
-
-let {ConsoleServiceListener, ConsoleAPIListener} =
- devtools.require("devtools/toolkit/webconsole/utils");
-
-function initCommon()
-{
- //Services.prefs.setBoolPref("devtools.debugger.log", true);
-
- Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
- Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
-}
-
-function initDebuggerServer()
-{
- if (!DebuggerServer.initialized) {
- DebuggerServer.init();
- DebuggerServer.addBrowserActors();
- }
-}
-
-function connectToDebugger(aCallback)
-{
- initCommon();
- initDebuggerServer();
-
- let transport = DebuggerServer.connectPipe();
- let client = new DebuggerClient(transport);
-
- let dbgState = { dbgClient: client };
- client.connect(aCallback.bind(null, dbgState));
-}
-
-function attachConsole(aListeners, aCallback, aAttachToTab)
-{
- function _onAttachConsole(aState, aResponse, aWebConsoleClient)
- {
- if (aResponse.error) {
- Cu.reportError("attachConsole failed: " + aResponse.error + " " +
- aResponse.message);
- }
-
- aState.client = aWebConsoleClient;
-
- aCallback(aState, aResponse);
- }
-
- connectToDebugger(function _onConnect(aState, aResponse) {
- if (aResponse.error) {
- Cu.reportError("client.connect() failed: " + aResponse.error + " " +
- aResponse.message);
- aCallback(aState, aResponse);
- return;
- }
-
- aState.dbgClient.listTabs(function _onListTabs(aResponse) {
- if (aResponse.error) {
- Cu.reportError("listTabs failed: " + aResponse.error + " " +
- aResponse.message);
- aCallback(aState, aResponse);
- return;
- }
- let consoleActor = aAttachToTab ?
- aResponse.tabs[aResponse.selected].consoleActor :
- aResponse.consoleActor;
- aState.actor = consoleActor;
- aState.dbgClient.attachConsole(consoleActor, aListeners,
- _onAttachConsole.bind(null, aState));
- });
- });
-}
-
-function closeDebugger(aState, aCallback)
-{
- aState.dbgClient.close(aCallback);
- aState.dbgClient = null;
- aState.client = null;
-}
-
-function checkConsoleAPICall(aCall, aExpected)
-{
- if (aExpected.level != "trace" && aExpected.arguments) {
- is(aCall.arguments.length, aExpected.arguments.length,
- "number of arguments");
- }
-
- checkObject(aCall, aExpected);
-}
-
-function checkObject(aObject, aExpected)
-{
- for (let name of Object.keys(aExpected))
- {
- let expected = aExpected[name];
- let value = aObject[name];
- checkValue(name, value, expected);
- }
-}
-
-function checkValue(aName, aValue, aExpected)
-{
- if (aExpected === null) {
- ok(!aValue, "'" + aName + "' is null");
- }
- else if (aValue === undefined) {
- ok(false, "'" + aName + "' is undefined");
- }
- else if (aValue === null) {
- ok(false, "'" + aName + "' is null");
- }
- else if (typeof aExpected == "string" || typeof aExpected == "number" ||
- typeof aExpected == "boolean") {
- is(aValue, aExpected, "property '" + aName + "'");
- }
- else if (aExpected instanceof RegExp) {
- ok(aExpected.test(aValue), aName + ": " + aExpected + " matched " + aValue);
- }
- else if (Array.isArray(aExpected)) {
- info("checking array for property '" + aName + "'");
- checkObject(aValue, aExpected);
- }
- else if (typeof aExpected == "object") {
- info("checking object for property '" + aName + "'");
- checkObject(aValue, aExpected);
- }
-}
-
-function checkHeadersOrCookies(aArray, aExpected)
-{
- let foundHeaders = {};
-
- for (let elem of aArray) {
- if (!(elem.name in aExpected)) {
- continue;
- }
- foundHeaders[elem.name] = true;
- info("checking value of header " + elem.name);
- checkValue(elem.name, elem.value, aExpected[elem.name]);
- }
-
- for (let header in aExpected) {
- if (!(header in foundHeaders)) {
- ok(false, header + " was not found");
- }
- }
-}
-
-function checkRawHeaders(aText, aExpected)
-{
- let headers = aText.split(/\r\n|\n|\r/);
- let arr = [];
- for (let header of headers) {
- let index = header.indexOf(": ");
- if (index < 0) {
- continue;
- }
- arr.push({
- name: header.substr(0, index),
- value: header.substr(index + 2)
- });
- }
-
- checkHeadersOrCookies(arr, aExpected);
-}
-
-var gTestState = {};
-
-function runTests(aTests, aEndCallback)
-{
- function driver()
- {
- let lastResult, sendToNext;
- for (let i = 0; i < aTests.length; i++) {
- gTestState.index = i;
- let fn = aTests[i];
- info("will run test #" + i + ": " + fn.name);
- lastResult = fn(sendToNext, lastResult);
- sendToNext = yield lastResult;
- }
- yield aEndCallback(sendToNext, lastResult);
- }
- gTestState.driver = driver();
- return gTestState.driver.next();
-}
-
-function nextTest(aMessage)
-{
- return gTestState.driver.send(aMessage);
-}
diff --git a/toolkit/devtools/webconsole/test/data.json b/toolkit/devtools/webconsole/test/data.json
deleted file mode 100644
index d46085c12..000000000
--- a/toolkit/devtools/webconsole/test/data.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ],
- veryLong: "foo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo bar"
-}
diff --git a/toolkit/devtools/webconsole/test/data.json^headers^ b/toolkit/devtools/webconsole/test/data.json^headers^
deleted file mode 100644
index bb6b45500..000000000
--- a/toolkit/devtools/webconsole/test/data.json^headers^
+++ /dev/null
@@ -1,3 +0,0 @@
-Content-Type: application/json
-x-very-long: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse a ipsum massa. Phasellus at elit dictum libero laoreet sagittis. Phasellus condimentum ultricies imperdiet. Nam eu ligula justo, ut tincidunt quam. Etiam sollicitudin, tortor sed egestas blandit, sapien sem tincidunt nulla, eu luctus libero odio quis leo. Nam elit massa, mattis quis blandit ac, facilisis vitae arcu. Donec vitae dictum neque. Proin ornare nisl at lectus commodo iaculis eget eget est. Quisque scelerisque vestibulum quam sed interdum.
-x-very-short: hello world
diff --git a/toolkit/devtools/webconsole/test/network_requests_iframe.html b/toolkit/devtools/webconsole/test/network_requests_iframe.html
deleted file mode 100644
index 0cca7f8b6..000000000
--- a/toolkit/devtools/webconsole/test/network_requests_iframe.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE HTML>
-<html>
- <head>
- <meta charset="utf-8">
- <title>Console HTTP test page</title>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
- <script type="text/javascript"><!--
- var setAllowAllCookies = false;
-
- function makeXhr(aMethod, aUrl, aRequestBody, aCallback) {
- // On the first call, allow all cookies and set cookies, then resume the actual test
- if(!setAllowAllCookies)
- SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]}, function () {
- setAllowAllCookies = true;
- setCookies();
- makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback);
- });
- else
- makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback);
- }
-
- function makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback) {
- var xmlhttp = new XMLHttpRequest();
- xmlhttp.open(aMethod, aUrl, true);
- if (aCallback) {
- xmlhttp.onreadystatechange = function() {
- if (xmlhttp.readyState == 4) {
- aCallback();
- }
- };
- }
- xmlhttp.send(aRequestBody);
- }
-
- function testXhrGet(aCallback) {
- makeXhr('get', 'data.json', null, aCallback);
- }
-
- function testXhrPost(aCallback) {
- var body = "Hello world! " + (new Array(50)).join("foobaz barr");
- makeXhr('post', 'data.json', body, aCallback);
- }
-
- function setCookies() {
- document.cookie = "foobar=fooval";
- document.cookie = "omgfoo=bug768096";
- document.cookie = "badcookie=bug826798=st3fan";
- }
- // --></script>
- </head>
- <body>
- <h1>Web Console HTTP Logging Testpage</h1>
- <h2>This page is used to test the HTTP logging.</h2>
-
- <form action="?" 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/toolkit/devtools/webconsole/test/sandboxed_iframe.html b/toolkit/devtools/webconsole/test/sandboxed_iframe.html
deleted file mode 100644
index 55a6224b5..000000000
--- a/toolkit/devtools/webconsole/test/sandboxed_iframe.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<html>
-<head><title>Sandboxed iframe</title></head>
-<body>
- <iframe id="sandboxed-iframe"
- sandbox="allow-scripts"
- srcdoc="<script>var foobarObject = {bug1051224: 'sandboxed'};</script>"></iframe>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_basics.html b/toolkit/devtools/webconsole/test/test_basics.html
deleted file mode 100644
index 26e77c07b..000000000
--- a/toolkit/devtools/webconsole/test/test_basics.html
+++ /dev/null
@@ -1,79 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Basic Web Console Actor tests</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Basic Web Console Actor tests</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["PageError"], onStartPageError);
-}
-
-function onStartPageError(aState, aResponse)
-{
- is(aResponse.startedListeners.length, 1, "startedListeners.length");
- is(aResponse.startedListeners[0], "PageError", "startedListeners: PageError");
- ok(aResponse.nativeConsoleAPI, "nativeConsoleAPI");
-
- closeDebugger(aState, function() {
- top.console_ = top.console;
- top.console = { lolz: "foo" };
- attachConsole(["PageError", "ConsoleAPI", "foo"],
- onStartPageErrorAndConsoleAPI, true);
- });
-}
-
-function onStartPageErrorAndConsoleAPI(aState, aResponse)
-{
- let startedListeners = aResponse.startedListeners;
- is(startedListeners.length, 2, "startedListeners.length");
- isnot(startedListeners.indexOf("PageError"), -1, "startedListeners: PageError");
- isnot(startedListeners.indexOf("ConsoleAPI"), -1,
- "startedListeners: ConsoleAPI");
- is(startedListeners.indexOf("foo"), -1, "startedListeners: no foo");
- ok(!aResponse.nativeConsoleAPI, "!nativeConsoleAPI");
-
- aState.client.stopListeners(["ConsoleAPI", "foo"],
- onStopConsoleAPI.bind(null, aState));
-}
-
-function onStopConsoleAPI(aState, aResponse)
-{
- is(aResponse.stoppedListeners.length, 1, "stoppedListeners.length");
- is(aResponse.stoppedListeners[0], "ConsoleAPI", "stoppedListeners: ConsoleAPI");
-
- closeDebugger(aState, function() {
- attachConsole(["ConsoleAPI"], onStartConsoleAPI);
- });
-}
-
-function onStartConsoleAPI(aState, aResponse)
-{
- is(aResponse.startedListeners.length, 1, "startedListeners.length");
- is(aResponse.startedListeners[0], "ConsoleAPI", "startedListeners: ConsoleAPI");
- ok(aResponse.nativeConsoleAPI, "nativeConsoleAPI");
-
- top.console = top.console_;
- delete top.console_;
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html b/toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html
deleted file mode 100644
index 0b5736a1d..000000000
--- a/toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html
+++ /dev/null
@@ -1,76 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for Bug 819670 - Web console object inspection does not handle native getters throwing very well</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for Bug 819670 - Web console object inspection does not handle native getters throwing very well</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
- attachConsole([], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onEvaluate = onEvaluate.bind(null, aState);
- aState.client.evaluateJS("document.__proto__", onEvaluate);
-}
-
-function onEvaluate(aState, aResponse)
-{
- checkObject(aResponse, {
- from: aState.actor,
- input: "document.__proto__",
- result: {
- type: "object",
- actor: /[a-z]/,
- },
- });
-
- ok(!aResponse.exception, "no eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- onInspect = onInspect.bind(null, aState);
- let client = new ObjectClient(aState.dbgClient, aResponse.result);
- client.getPrototypeAndProperties(onInspect);
-}
-
-function onInspect(aState, aResponse)
-{
- ok(!aResponse.error, "no response error");
-
- let expectedProps = {
- "addBroadcastListenerFor": { value: { type: "object" } },
- "commandDispatcher": { get: { type: "object" } },
- "getBoxObjectFor": { value: { type: "object" } },
- "getElementsByAttribute": { value: { type: "object" } },
- };
-
- let props = aResponse.ownProperties;
- ok(props, "response properties available");
-
- if (props) {
- ok(Object.keys(props).length > Object.keys(expectedProps).length,
- "number of enumerable properties");
- checkObject(props, expectedProps);
- }
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_cached_messages.html b/toolkit/devtools/webconsole/test/test_cached_messages.html
deleted file mode 100644
index 210543ca3..000000000
--- a/toolkit/devtools/webconsole/test/test_cached_messages.html
+++ /dev/null
@@ -1,230 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for cached messages</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for cached messages</p>
-
-<script class="testbody" type="application/javascript;version=1.8">
-let expectedConsoleCalls = [];
-let expectedPageErrors = [];
-
-function doPageErrors()
-{
- Services.console.reset();
-
- expectedPageErrors = [
- {
- _type: "PageError",
- errorMessage: /fooColor/,
- sourceName: /.+/,
- category: "CSS Parser",
- timeStamp: /^\d+$/,
- error: false,
- warning: true,
- exception: false,
- strict: false,
- },
- {
- _type: "PageError",
- errorMessage: /doTheImpossible/,
- sourceName: /.+/,
- category: "chrome javascript",
- timeStamp: /^\d+$/,
- error: false,
- warning: false,
- exception: true,
- strict: false,
- },
- ];
-
- let container = document.createElement("script");
- document.body.appendChild(container);
- container.textContent = "document.body.style.color = 'fooColor';";
- document.body.removeChild(container);
-
- SimpleTest.expectUncaughtException();
-
- container = document.createElement("script");
- document.body.appendChild(container);
- container.textContent = "document.doTheImpossible();";
- document.body.removeChild(container);
-}
-
-function doConsoleCalls()
-{
- ConsoleAPIStorage.clearEvents();
-
- top.console.log("foobarBaz-log", undefined);
- top.console.info("foobarBaz-info", null);
- top.console.warn("foobarBaz-warn", document.body);
-
- expectedConsoleCalls = [
- {
- _type: "ConsoleAPI",
- level: "log",
- filename: /test_cached_messages/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-log", { type: "undefined" }],
- },
- {
- _type: "ConsoleAPI",
- level: "info",
- filename: /test_cached_messages/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-info", { type: "null" }],
- },
- {
- _type: "ConsoleAPI",
- level: "warn",
- filename: /test_cached_messages/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
- },
- ];
-}
-</script>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let consoleAPIListener, consoleServiceListener;
-let consoleAPICalls = 0;
-let pageErrors = 0;
-
-let handlers = {
- onConsoleAPICall: function onConsoleAPICall(aMessage)
- {
- for (let msg of expectedConsoleCalls) {
- if (msg.functionName == aMessage.functionName &&
- msg.filename.test(aMessage.filename)) {
- consoleAPICalls++;
- break;
- }
- }
- if (consoleAPICalls == expectedConsoleCalls.length) {
- checkConsoleAPICache();
- }
- },
-
- onConsoleServiceMessage: function onConsoleServiceMessage(aMessage)
- {
- if (!(aMessage instanceof Ci.nsIScriptError)) {
- return;
- }
- for (let msg of expectedPageErrors) {
- if (msg.category == aMessage.category &&
- msg.errorMessage.test(aMessage.errorMessage)) {
- pageErrors++;
- break;
- }
- }
- if (pageErrors == expectedPageErrors.length) {
- testPageErrors();
- }
- },
-};
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- consoleAPIListener = new ConsoleAPIListener(top, handlers);
- consoleAPIListener.init();
-
- doConsoleCalls();
-}
-
-function checkConsoleAPICache()
-{
- consoleAPIListener.destroy();
- consoleAPIListener = null;
- attachConsole(["ConsoleAPI"], onAttach1);
-}
-
-function onAttach1(aState, aResponse)
-{
- aState.client.getCachedMessages(["ConsoleAPI"],
- onCachedConsoleAPI.bind(null, aState));
-}
-
-function onCachedConsoleAPI(aState, aResponse)
-{
- let msgs = aResponse.messages;
- info("cached console messages: " + msgs.length);
-
- ok(msgs.length >= expectedConsoleCalls.length,
- "number of cached console messages");
-
- for (let msg of msgs) {
- for (let expected of expectedConsoleCalls) {
- if (expected.functionName == msg.functionName &&
- expected.filename.test(msg.filename)) {
- expectedConsoleCalls.splice(expectedConsoleCalls.indexOf(expected));
- checkConsoleAPICall(msg, expected);
- break;
- }
- }
- }
-
- is(expectedConsoleCalls.length, 0, "all expected messages have been found");
-
- closeDebugger(aState, function() {
- consoleServiceListener = new ConsoleServiceListener(null, handlers);
- consoleServiceListener.init();
- doPageErrors();
- });
-}
-
-function testPageErrors()
-{
- consoleServiceListener.destroy();
- consoleServiceListener = null;
- attachConsole(["PageError"], onAttach2);
-}
-
-function onAttach2(aState, aResponse)
-{
- aState.client.getCachedMessages(["PageError"],
- onCachedPageErrors.bind(null, aState));
-}
-
-function onCachedPageErrors(aState, aResponse)
-{
- let msgs = aResponse.messages;
- info("cached page errors: " + msgs.length);
-
- ok(msgs.length >= expectedPageErrors.length,
- "number of cached page errors");
-
- for (let msg of msgs) {
- for (let expected of expectedPageErrors) {
- if (expected.category == msg.category &&
- expected.errorMessage.test(msg.errorMessage)) {
- expectedPageErrors.splice(expectedPageErrors.indexOf(expected));
- checkObject(msg, expected);
- break;
- }
- }
- }
-
- is(expectedPageErrors.length, 0, "all expected messages have been found");
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_consoleapi.html b/toolkit/devtools/webconsole/test/test_consoleapi.html
deleted file mode 100644
index 4a8dea0e0..000000000
--- a/toolkit/devtools/webconsole/test/test_consoleapi.html
+++ /dev/null
@@ -1,199 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the Console API</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the Console API</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let expectedConsoleCalls = [];
-
-function doConsoleCalls(aState)
-{
- let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a");
-
- top.console.log("foobarBaz-log", undefined);
- top.console.info("foobarBaz-info", null);
- top.console.warn("foobarBaz-warn", top.document.documentElement);
- top.console.debug(null);
- top.console.trace();
- top.console.dir(top.document, top.location);
- top.console.log("foo", longString);
-
- function fromAsmJS() {
- top.console.error("foobarBaz-asmjs-error", undefined);
- }
-
- (function(global, foreign) {
- "use asm";
- var fromAsmJS = foreign.fromAsmJS;
- function inAsmJS2() { fromAsmJS() }
- function inAsmJS1() { inAsmJS2() }
- return inAsmJS1
- })(null, { fromAsmJS:fromAsmJS })();
-
- expectedConsoleCalls = [
- {
- level: "log",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-log", { type: "undefined" }],
- },
- {
- level: "info",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-info", { type: "null" }],
- },
- {
- level: "warn",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
- },
- {
- level: "debug",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [{ type: "null" }],
- },
- {
- level: "trace",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- stacktrace: [
- {
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- },
- {
- filename: /test_consoleapi/,
- functionName: "onAttach",
- },
- ],
- },
- {
- level: "dir",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [
- {
- type: "object",
- actor: /[a-z]/,
- class: "XULDocument",
- },
- {
- type: "object",
- actor: /[a-z]/,
- class: "Location",
- }
- ],
- },
- {
- level: "log",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [
- "foo",
- {
- type: "longString",
- initial: longString.substring(0,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH),
- length: longString.length,
- actor: /[a-z]/,
- },
- ],
- },
- {
- level: "error",
- filename: /test_consoleapi/,
- functionName: "fromAsmJS",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-asmjs-error", { type: "undefined" }],
-
- stacktrace: [
- {
- filename: /test_consoleapi/,
- functionName: "fromAsmJS",
- },
- {
- filename: /test_consoleapi/,
- functionName: "inAsmJS2",
- },
- {
- filename: /test_consoleapi/,
- functionName: "inAsmJS1",
- },
- {
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- },
- {
- filename: /test_consoleapi/,
- functionName: "onAttach",
- },
- ],
- },
- ];
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["ConsoleAPI"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onConsoleAPICall = onConsoleAPICall.bind(null, aState);
- aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall);
- doConsoleCalls(aState.actor);
-}
-
-let consoleCalls = [];
-
-function onConsoleAPICall(aState, aType, aPacket)
-{
- info("received message level: " + aPacket.message.level);
- is(aPacket.from, aState.actor, "console API call actor");
-
- consoleCalls.push(aPacket.message);
- if (consoleCalls.length != expectedConsoleCalls.length) {
- return;
- }
-
- aState.dbgClient.removeListener("consoleAPICall", onConsoleAPICall);
-
- expectedConsoleCalls.forEach(function(aMessage, aIndex) {
- info("checking received console call #" + aIndex);
- checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]);
- });
-
-
- consoleCalls = [];
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_consoleapi_innerID.html b/toolkit/devtools/webconsole/test/test_consoleapi_innerID.html
deleted file mode 100644
index 49b9fa50e..000000000
--- a/toolkit/devtools/webconsole/test/test_consoleapi_innerID.html
+++ /dev/null
@@ -1,164 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the innerID property of the Console API</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the Console API</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let expectedConsoleCalls = [];
-
-function doConsoleCalls(aState)
-{
- let { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
- let console = new ConsoleAPI({
- innerID: window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils)
- .currentInnerWindowID
- });
-
- let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a");
-
- console.log("foobarBaz-log", undefined);
- console.info("foobarBaz-info", null);
- console.warn("foobarBaz-warn", top.document.documentElement);
- console.debug(null);
- console.trace();
- console.dir(top.document, top.location);
- console.log("foo", longString);
-
- expectedConsoleCalls = [
- {
- level: "log",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-log", { type: "undefined" }],
- },
- {
- level: "info",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-info", { type: "null" }],
- },
- {
- level: "warn",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
- },
- {
- level: "debug",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [{ type: "null" }],
- },
- {
- level: "trace",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- stacktrace: [
- {
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- },
- {
- filename: /test_consoleapi/,
- functionName: "onAttach",
- },
- ],
- },
- {
- level: "dir",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [
- {
- type: "object",
- actor: /[a-z]/,
- class: "XULDocument",
- },
- {
- type: "object",
- actor: /[a-z]/,
- class: "Location",
- }
- ],
- },
- {
- level: "log",
- filename: /test_consoleapi/,
- functionName: "doConsoleCalls",
- timeStamp: /^\d+$/,
- arguments: [
- "foo",
- {
- type: "longString",
- initial: longString.substring(0,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH),
- length: longString.length,
- actor: /[a-z]/,
- },
- ],
- },
- ];
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["ConsoleAPI"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onConsoleAPICall = onConsoleAPICall.bind(null, aState);
- aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall);
- doConsoleCalls(aState.actor);
-}
-
-let consoleCalls = [];
-
-function onConsoleAPICall(aState, aType, aPacket)
-{
- info("received message level: " + aPacket.message.level);
- is(aPacket.from, aState.actor, "console API call actor");
-
- consoleCalls.push(aPacket.message);
- if (consoleCalls.length != expectedConsoleCalls.length) {
- return;
- }
-
- aState.dbgClient.removeListener("consoleAPICall", onConsoleAPICall);
-
- expectedConsoleCalls.forEach(function(aMessage, aIndex) {
- info("checking received console call #" + aIndex);
- checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]);
- });
-
-
- consoleCalls = [];
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_file_uri.html b/toolkit/devtools/webconsole/test/test_file_uri.html
deleted file mode 100644
index f5aada5b1..000000000
--- a/toolkit/devtools/webconsole/test/test_file_uri.html
+++ /dev/null
@@ -1,106 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for file activity tracking</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for file activity tracking</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-
-let gState;
-let gTmpFile;
-
-function doFileActivity()
-{
- info("doFileActivity");
- let fileContent = "<p>hello world from bug 798764";
-
- gTmpFile = FileUtils.getFile("TmpD", ["bug798764.html"]);
- gTmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
- let fout = FileUtils.openSafeFileOutputStream(gTmpFile,
- FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
-
- let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
- createInstance(Ci.nsIScriptableUnicodeConverter);
- converter.charset = "UTF-8";
- let fileContentStream = converter.convertToInputStream(fileContent);
-
- NetUtil.asyncCopy(fileContentStream, fout, addIframe);
-}
-
-function addIframe(aStatus)
-{
- ok(Components.isSuccessCode(aStatus),
- "the temporary file was saved successfully");
-
- let iframe = document.createElement("iframe");
- iframe.src = NetUtil.newURI(gTmpFile).spec;
- document.body.appendChild(iframe);
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["FileActivity"], onAttach);
-}
-
-function onAttach(aState, aResponse)
-{
- gState = aState;
- gState.dbgClient.addListener("fileActivity", onFileActivity);
- doFileActivity();
-}
-
-function onFileActivity(aType, aPacket)
-{
- is(aPacket.from, gState.actor, "fileActivity actor");
-
- gState.dbgClient.removeListener("fileActivity", onFileActivity);
-
- info("aPacket.uri: " + aPacket.uri);
- ok(/\bbug798764\b.*\.html$/.test(aPacket.uri), "file URI match");
-
- testEnd();
-}
-
-function testEnd()
-{
- if (gTmpFile) {
- SimpleTest.executeSoon(function() {
- try {
- gTmpFile.remove(false);
- }
- catch (ex if (ex.name == "NS_ERROR_FILE_IS_LOCKED")) {
- // Sometimes remove() throws because the file is not unlocked soon
- // enough.
- }
- gTmpFile = null;
- });
- }
-
- if (gState) {
- closeDebugger(gState, function() {
- gState = null;
- SimpleTest.finish();
- });
- } else {
- SimpleTest.finish();
- }
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_jsterm.html b/toolkit/devtools/webconsole/test/test_jsterm.html
deleted file mode 100644
index a7022fd26..000000000
--- a/toolkit/devtools/webconsole/test/test_jsterm.html
+++ /dev/null
@@ -1,319 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for JavaScript terminal functionality</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for JavaScript terminal functionality</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let gState;
-
-let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-let {MAX_AUTOCOMPLETE_ATTEMPTS,MAX_AUTOCOMPLETIONS} = devtools.require("devtools/toolkit/webconsole/utils");
-
-// This test runs all of its assertions twice - once with
-// evaluateJS and once with evaluateJSAsync.
-let evaluatingSync = true;
-function evaluateJS(input, callback) {
- if (evaluatingSync) {
- gState.client.evaluateJS(input, callback);
- } else {
- gState.client.evaluateJSAsync(input, callback);
- }
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["PageError"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- top.foobarObject = Object.create(null);
- top.foobarObject.foo = 1;
- top.foobarObject.foobar = 2;
- top.foobarObject.foobaz = 3;
- top.foobarObject.omg = 4;
- top.foobarObject.omgfoo = 5;
- top.foobarObject.strfoo = "foobarz";
- top.foobarObject.omgstr = "foobarz" +
- (new Array(DebuggerServer.LONG_STRING_LENGTH * 2)).join("abb");
-
- top.largeObject1 = Object.create(null);
- for (let i = 0; i < MAX_AUTOCOMPLETE_ATTEMPTS + 1; i++) {
- top.largeObject1['a' + i] = i;
- }
-
- top.largeObject2 = Object.create(null);
- for (let i = 0; i < MAX_AUTOCOMPLETIONS * 2; i++) {
- top.largeObject2['a' + i] = i;
- }
-
- gState = aState;
-
- let tests = [doAutocomplete1, doAutocomplete2, doAutocomplete3,
- doAutocomplete4, doAutocompleteLarge1, doAutocompleteLarge2,
- doSimpleEval, doWindowEval, doEvalWithException,
- doEvalWithHelper, doEvalString, doEvalLongString];
- runTests(tests, testEnd);
-}
-
-function doAutocomplete1()
-{
- info("test autocomplete for 'window.foo'");
- gState.client.autocomplete("window.foo", 10, onAutocomplete1);
-}
-
-function onAutocomplete1(aResponse)
-{
- let matches = aResponse.matches;
-
- is(aResponse.matchProp, "foo", "matchProp");
- is(matches.length, 1, "matches.length");
- is(matches[0], "foobarObject", "matches[0]");
-
- nextTest();
-}
-
-function doAutocomplete2()
-{
- info("test autocomplete for 'window.foobarObject.'");
- gState.client.autocomplete("window.foobarObject.", 20, onAutocomplete2);
-}
-
-function onAutocomplete2(aResponse)
-{
- let matches = aResponse.matches;
-
- ok(!aResponse.matchProp, "matchProp");
- is(matches.length, 7, "matches.length");
- checkObject(matches,
- ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
-
- nextTest();
-}
-
-function doAutocomplete3()
-{
- // Check that completion suggestions are offered inside the string.
- info("test autocomplete for 'dump(window.foobarObject.)'");
- gState.client.autocomplete("dump(window.foobarObject.)", 25, onAutocomplete3);
-}
-
-function onAutocomplete3(aResponse)
-{
- let matches = aResponse.matches;
-
- ok(!aResponse.matchProp, "matchProp");
- is(matches.length, 7, "matches.length");
- checkObject(matches,
- ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
-
- nextTest();
-}
-
-function doAutocomplete4()
-{
- // Check that completion requests can have no suggestions.
- info("test autocomplete for 'dump(window.foobarObject.)'");
- gState.client.autocomplete("dump(window.foobarObject.)", 26, onAutocomplete4);
-}
-
-function onAutocomplete4(aResponse)
-{
- ok(!aResponse.matchProp, "matchProp");
- is(aResponse.matches.length, 0, "matches.length");
-
- nextTest();
-}
-
-function doAutocompleteLarge1()
-{
- // Check that completion requests with too large objects will
- // have no suggestions.
- info("test autocomplete for 'window.largeObject1.'");
- gState.client.autocomplete("window.largeObject1.", 20, onAutocompleteLarge1);
-}
-
-function onAutocompleteLarge1(aResponse)
-{
- ok(!aResponse.matchProp, "matchProp");
- info (aResponse.matches.join("|"));
- is(aResponse.matches.length, 0, "Bailed out with too many properties");
-
- nextTest();
-}
-
-function doAutocompleteLarge2()
-{
- // Check that completion requests with pretty large objects will
- // have MAX_AUTOCOMPLETIONS suggestions
- info("test autocomplete for 'window.largeObject2.'");
- gState.client.autocomplete("window.largeObject2.", 20, onAutocompleteLarge2);
-}
-
-function onAutocompleteLarge2(aResponse)
-{
- ok(!aResponse.matchProp, "matchProp");
- is(aResponse.matches.length, MAX_AUTOCOMPLETIONS, "matches.length is MAX_AUTOCOMPLETIONS");
-
- nextTest();
-}
-
-function doSimpleEval()
-{
- info("test eval '2+2'");
- evaluateJS("2+2", onSimpleEval);
-}
-
-function onSimpleEval(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "2+2",
- result: 4,
- });
-
- ok(!aResponse.exception, "no eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doWindowEval()
-{
- info("test eval 'document'");
- evaluateJS("document", onWindowEval);
-}
-
-function onWindowEval(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "document",
- result: {
- type: "object",
- class: "XULDocument",
- actor: /[a-z]/,
- },
- });
-
- ok(!aResponse.exception, "no eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doEvalWithException()
-{
- info("test eval with exception");
- evaluateJS("window.doTheImpossible()", onEvalWithException);
-}
-
-function onEvalWithException(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.doTheImpossible()",
- result: {
- type: "undefined",
- },
- exceptionMessage: /doTheImpossible/,
- });
-
- ok(aResponse.exception, "js eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doEvalWithHelper()
-{
- info("test eval with helper");
- evaluateJS("clear()", onEvalWithHelper);
-}
-
-function onEvalWithHelper(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "clear()",
- result: {
- type: "undefined",
- },
- helperResult: { type: "clearOutput" },
- });
-
- ok(!aResponse.exception, "no eval exception");
-
- nextTest();
-}
-
-function doEvalString()
-{
- evaluateJS("window.foobarObject.strfoo", onEvalString);
-}
-
-function onEvalString(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.strfoo",
- result: "foobarz",
- });
-
- nextTest();
-}
-
-function doEvalLongString()
-{
- evaluateJS("window.foobarObject.omgstr", onEvalLongString);
-}
-
-function onEvalLongString(aResponse)
-{
- let str = top.foobarObject.omgstr;
- let initial = str.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
-
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.omgstr",
- result: {
- type: "longString",
- initial: initial,
- length: str.length,
- },
- });
-
- nextTest();
-}
-
-function testEnd()
-{
- // If this is the first run, reload the page and do it again.
- // Otherwise, end the test.
- closeDebugger(gState, function() {
- gState = null;
- if (evaluatingSync) {
- evaluatingSync = false;
- startTest();
- } else {
- SimpleTest.finish();
- }
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html b/toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html
deleted file mode 100644
index 034e99ff4..000000000
--- a/toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html
+++ /dev/null
@@ -1,223 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the cd() function</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the cd() function</p>
-
-<iframe id="content-iframe" src="http://example.com/chrome/toolkit/devtools/webconsole/test/sandboxed_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let gState;
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole([], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- top.foobarObject = Object.create(null);
- top.foobarObject.bug609872 = "parent";
-
- window.foobarObject = Object.create(null);
- window.foobarObject.bug609872 = "child";
-
- gState = aState;
-
- let tests = [doCheckParent, doCdIframe, doCheckIframe,
- doCdContentIframe,
- doCdSandboxedIframe, doCheckSandboxedIframe,
- doCdParent,
- doCdParent,
- doCheckParent2];
- runTests(tests, testEnd);
-}
-
-function doCheckParent()
-{
- info("check parent window");
- gState.client.evaluateJS("window.foobarObject.bug609872",
- onFooObjectFromParent);
-}
-
-function onFooObjectFromParent(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.bug609872",
- result: "parent",
- });
-
- ok(!aResponse.exception, "no eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doCdIframe()
-{
- info("test cd('iframe')");
- gState.client.evaluateJS("cd('iframe')", onCdIframe);
-}
-
-function onCdIframe(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "cd('iframe')",
- result: { type: "undefined" },
- helperResult: { type: "cd" },
- });
-
- ok(!aResponse.exception, "no eval exception");
-
- nextTest();
-}
-
-function doCheckIframe()
-{
- info("check foobarObject from the iframe");
- gState.client.evaluateJS("window.foobarObject.bug609872",
- onFooObjectFromIframe);
-}
-
-function onFooObjectFromIframe(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.bug609872",
- result: "child",
- });
-
- ok(!aResponse.exception, "no js eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doCdContentIframe()
-{
- info("test cd('#content-iframe')");
- gState.client.evaluateJS("cd('#content-iframe')", onCdContentIframe);
-}
-
-function onCdContentIframe(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "cd('#content-iframe')",
- result: { type: "undefined" },
- helperResult: { type: "cd" },
- });
-
- ok(!aResponse.exception, "no eval exception");
-
- nextTest();
-}
-function doCdSandboxedIframe()
-{
- // Don't use string to ensure we don't get security exception
- // when passing a content window reference.
- let cmd = "cd(document.getElementById('sandboxed-iframe').contentWindow)";
- info("test " + cmd);
- gState.client.evaluateJS(cmd, onCdSandboxedIframe.bind(null, cmd));
-}
-
-function onCdSandboxedIframe(cmd, aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: cmd,
- result: { type: "undefined" },
- helperResult: { type: "cd" },
- });
-
- ok(!aResponse.exception, "no eval exception");
-
- nextTest();
-}
-
-function doCheckSandboxedIframe()
-{
- info("check foobarObject from the sandboxed iframe");
- gState.client.evaluateJS("window.foobarObject.bug1051224",
- onFooObjectFromSandboxedIframe);
-}
-
-function onFooObjectFromSandboxedIframe(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.bug1051224",
- result: "sandboxed",
- });
-
- ok(!aResponse.exception, "no js eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function doCdParent()
-{
- info("test cd() back to parent");
- gState.client.evaluateJS("cd()", onCdParent);
-}
-
-function onCdParent(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "cd()",
- result: { type: "undefined" },
- helperResult: { type: "cd" },
- });
-
- ok(!aResponse.exception, "no eval exception");
-
- nextTest();
-}
-
-function doCheckParent2()
-{
- gState.client.evaluateJS("window.foobarObject.bug609872",
- onFooObjectFromParent2);
-}
-
-function onFooObjectFromParent2(aResponse)
-{
- checkObject(aResponse, {
- from: gState.actor,
- input: "window.foobarObject.bug609872",
- result: "parent",
- });
-
- ok(!aResponse.exception, "no eval exception");
- ok(!aResponse.helperResult, "no helper result");
-
- nextTest();
-}
-
-function testEnd()
-{
- closeDebugger(gState, function() {
- gState = null;
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_network_get.html b/toolkit/devtools/webconsole/test/test_network_get.html
deleted file mode 100644
index 0c3c50da0..000000000
--- a/toolkit/devtools/webconsole/test/test_network_get.html
+++ /dev/null
@@ -1,260 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the network actor (GET request)</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the network actor (GET request)</p>
-
-<iframe src="http://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
- attachConsole(["NetworkActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- info("test network GET request");
-
- onNetworkEvent = onNetworkEvent.bind(null, aState);
- aState.dbgClient.addListener("networkEvent", onNetworkEvent);
- onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
- aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
-
- let iframe = document.querySelector("iframe").contentWindow;
- iframe.wrappedJSObject.testXhrGet();
-}
-
-function onNetworkEvent(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "network event actor");
-
- info("checking the network event packet");
-
- let netActor = aPacket.eventActor;
-
- checkObject(netActor, {
- actor: /[a-z]/,
- startedDateTime: /^\d+\-\d+\-\d+T.+$/,
- url: /data\.json/,
- method: "GET",
- });
-
- aState.netActor = netActor.actor;
-
- aState.dbgClient.removeListener("networkEvent", onNetworkEvent);
-}
-
-let updates = [];
-
-function onNetworkEventUpdate(aState, aType, aPacket)
-{
- info("received networkEventUpdate " + aPacket.updateType);
- is(aPacket.from, aState.netActor, "networkEventUpdate actor");
-
- updates.push(aPacket.updateType);
-
- let expectedPacket = null;
-
- switch (aPacket.updateType) {
- case "requestHeaders":
- case "responseHeaders":
- ok(aPacket.headers > 0, "headers > 0");
- ok(aPacket.headersSize > 0, "headersSize > 0");
- break;
- case "requestCookies":
- expectedPacket = {
- cookies: 3,
- };
- break;
- case "requestPostData":
- ok(false, "got unexpected requestPostData");
- break;
- case "responseStart":
- expectedPacket = {
- response: {
- httpVersion: /^HTTP\/\d\.\d$/,
- status: 200,
- statusText: "OK",
- headersSize: /^\d+$/,
- discardResponseBody: true,
- },
- };
- break;
- case "securityInfo":
- expectedPacket = {
- state: "insecure",
- };
- break;
- case "responseCookies":
- expectedPacket = {
- cookies: 0,
- };
- break;
- case "responseContent":
- expectedPacket = {
- mimeType: "application/json",
- contentSize: 0,
- discardResponseBody: true,
- };
- break;
- case "eventTimings":
- expectedPacket = {
- totalTime: /^\d+$/,
- };
- break;
- default:
- ok(false, "unknown network event update type: " +
- aPacket.updateType);
- return;
- }
-
- if (expectedPacket) {
- info("checking the packet content");
- checkObject(aPacket, expectedPacket);
- }
-
- if (updates.indexOf("responseContent") > -1 &&
- updates.indexOf("eventTimings") > -1) {
- aState.dbgClient.removeListener("networkEventUpdate",
- onNetworkEvent);
-
- onRequestHeaders = onRequestHeaders.bind(null, aState);
- aState.client.getRequestHeaders(aState.netActor,
- onRequestHeaders);
- }
-}
-
-function onRequestHeaders(aState, aResponse)
-{
- info("checking request headers");
-
- ok(aResponse.headers.length > 0, "request headers > 0");
- ok(aResponse.headersSize > 0, "request headersSize > 0");
- ok(!!aResponse.rawHeaders, "request rawHeaders available");
-
- checkHeadersOrCookies(aResponse.headers, {
- Referer: /network_requests_iframe\.html/,
- Cookie: /bug768096/,
- });
-
- checkRawHeaders(aResponse.rawHeaders, {
- Referer: /network_requests_iframe\.html/,
- Cookie: /bug768096/,
- });
-
- onRequestCookies = onRequestCookies.bind(null, aState);
- aState.client.getRequestCookies(aState.netActor,
- onRequestCookies);
-}
-
-function onRequestCookies(aState, aResponse)
-{
- info("checking request cookies");
-
- is(aResponse.cookies.length, 3, "request cookies length");
-
- checkHeadersOrCookies(aResponse.cookies, {
- foobar: "fooval",
- omgfoo: "bug768096",
- badcookie: "bug826798=st3fan",
- });
-
- onRequestPostData = onRequestPostData.bind(null, aState);
- aState.client.getRequestPostData(aState.netActor,
- onRequestPostData);
-}
-
-function onRequestPostData(aState, aResponse)
-{
- info("checking request POST data");
-
- ok(!aResponse.postData.text, "no request POST data");
- ok(aResponse.postDataDiscarded, "request POST data was discarded");
-
- onResponseHeaders = onResponseHeaders.bind(null, aState);
- aState.client.getResponseHeaders(aState.netActor,
- onResponseHeaders);
-}
-
-function onResponseHeaders(aState, aResponse)
-{
- info("checking response headers");
-
- ok(aResponse.headers.length > 0, "response headers > 0");
- ok(aResponse.headersSize > 0, "response headersSize > 0");
- ok(!!aResponse.rawHeaders, "response rawHeaders available");
-
- checkHeadersOrCookies(aResponse.headers, {
- "Content-Type": /^application\/(json|octet-stream)$/,
- "Content-Length": /^\d+$/,
- });
-
- checkRawHeaders(aResponse.rawHeaders, {
- "Content-Type": /^application\/(json|octet-stream)$/,
- "Content-Length": /^\d+$/,
- });
-
- onResponseCookies = onResponseCookies.bind(null, aState);
- aState.client.getResponseCookies(aState.netActor,
- onResponseCookies);
-}
-
-function onResponseCookies(aState, aResponse)
-{
- info("checking response cookies");
-
- is(aResponse.cookies.length, 0, "response cookies length");
-
- onResponseContent = onResponseContent.bind(null, aState);
- aState.client.getResponseContent(aState.netActor,
- onResponseContent);
-}
-
-function onResponseContent(aState, aResponse)
-{
- info("checking response content");
-
- ok(!aResponse.content.text, "no response content");
- ok(aResponse.contentDiscarded, "response content was discarded");
-
- onEventTimings = onEventTimings.bind(null, aState);
- aState.client.getEventTimings(aState.netActor,
- onEventTimings);
-}
-
-function onEventTimings(aState, aResponse)
-{
- info("checking event timings");
-
- checkObject(aResponse, {
- timings: {
- blocked: /^-1|\d+$/,
- dns: /^-1|\d+$/,
- connect: /^-1|\d+$/,
- send: /^-1|\d+$/,
- wait: /^-1|\d+$/,
- receive: /^-1|\d+$/,
- },
- totalTime: /^\d+$/,
- });
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_network_longstring.html b/toolkit/devtools/webconsole/test/test_network_longstring.html
deleted file mode 100644
index 1dc7bc762..000000000
--- a/toolkit/devtools/webconsole/test/test_network_longstring.html
+++ /dev/null
@@ -1,305 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test that the network actor uses the LongStringActor</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test that the network actor uses the LongStringActor</p>
-
-<iframe src="http://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["NetworkActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- info("enable network request and response body logging");
-
- window.ORIGINAL_LONG_STRING_LENGTH = DebuggerServer.LONG_STRING_LENGTH;
- window.ORIGINAL_LONG_STRING_INITIAL_LENGTH =
- DebuggerServer.LONG_STRING_INITIAL_LENGTH;
-
- DebuggerServer.LONG_STRING_LENGTH = 400;
- DebuggerServer.LONG_STRING_INITIAL_LENGTH = 400;
-
- onSetPreferences = onSetPreferences.bind(null, aState);
- aState.client.setPreferences({
- "NetworkMonitor.saveRequestAndResponseBodies": true,
- }, onSetPreferences);
-}
-
-function onSetPreferences(aState, aResponse)
-{
- is(aResponse.updated.length, 1, "updated prefs length");
- is(aResponse.updated[0], "NetworkMonitor.saveRequestAndResponseBodies",
- "updated prefs length");
-
- info("test network POST request");
-
- onNetworkEvent = onNetworkEvent.bind(null, aState);
- aState.dbgClient.addListener("networkEvent", onNetworkEvent);
- onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
- aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
-
- let iframe = document.querySelector("iframe").contentWindow;
- iframe.wrappedJSObject.testXhrPost();
-}
-
-function onNetworkEvent(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "network event actor");
-
- info("checking the network event packet");
-
- let netActor = aPacket.eventActor;
-
- checkObject(netActor, {
- actor: /[a-z]/,
- startedDateTime: /^\d+\-\d+\-\d+T.+$/,
- url: /data\.json/,
- method: "POST",
- });
-
- aState.netActor = netActor.actor;
-
- aState.dbgClient.removeListener("networkEvent", onNetworkEvent);
-}
-
-let updates = [];
-
-function onNetworkEventUpdate(aState, aType, aPacket)
-{
- info("received networkEventUpdate " + aPacket.updateType);
- is(aPacket.from, aState.netActor, "networkEventUpdate actor");
-
- updates.push(aPacket.updateType);
-
- let expectedPacket = null;
-
- switch (aPacket.updateType) {
- case "requestHeaders":
- case "responseHeaders":
- ok(aPacket.headers > 0, "headers > 0");
- ok(aPacket.headersSize > 0, "headersSize > 0");
- break;
- case "requestCookies":
- expectedPacket = {
- cookies: 3,
- };
- break;
- case "requestPostData":
- ok(aPacket.dataSize > 0, "dataSize > 0");
- ok(!aPacket.discardRequestBody, "discardRequestBody");
- break;
- case "responseStart":
- expectedPacket = {
- response: {
- httpVersion: /^HTTP\/\d\.\d$/,
- status: 200,
- statusText: "OK",
- headersSize: /^\d+$/,
- discardResponseBody: false,
- },
- };
- break;
- case "securityInfo":
- expectedPacket = {
- state: "insecure",
- };
- break;
- case "responseCookies":
- expectedPacket = {
- cookies: 0,
- };
- break;
- case "responseContent":
- expectedPacket = {
- mimeType: "application/json",
- contentSize: /^\d+$/,
- discardResponseBody: false,
- };
- break;
- case "eventTimings":
- expectedPacket = {
- totalTime: /^\d+$/,
- };
- break;
- default:
- ok(false, "unknown network event update type: " +
- aPacket.updateType);
- return;
- }
-
- if (expectedPacket) {
- info("checking the packet content");
- checkObject(aPacket, expectedPacket);
- }
-
- if (updates.indexOf("responseContent") > -1 &&
- updates.indexOf("eventTimings") > -1) {
- aState.dbgClient.removeListener("networkEventUpdate",
- onNetworkEvent);
-
- onRequestHeaders = onRequestHeaders.bind(null, aState);
- aState.client.getRequestHeaders(aState.netActor,
- onRequestHeaders);
- }
-}
-
-function onRequestHeaders(aState, aResponse)
-{
- info("checking request headers");
-
- ok(aResponse.headers.length > 0, "request headers > 0");
- ok(aResponse.headersSize > 0, "request headersSize > 0");
-
- checkHeadersOrCookies(aResponse.headers, {
- Referer: /network_requests_iframe\.html/,
- Cookie: /bug768096/,
- });
-
- onRequestCookies = onRequestCookies.bind(null, aState);
- aState.client.getRequestCookies(aState.netActor,
- onRequestCookies);
-}
-
-function onRequestCookies(aState, aResponse)
-{
- info("checking request cookies");
-
- is(aResponse.cookies.length, 3, "request cookies length");
-
- checkHeadersOrCookies(aResponse.cookies, {
- foobar: "fooval",
- omgfoo: "bug768096",
- badcookie: "bug826798=st3fan",
- });
-
- onRequestPostData = onRequestPostData.bind(null, aState);
- aState.client.getRequestPostData(aState.netActor,
- onRequestPostData);
-}
-
-function onRequestPostData(aState, aResponse)
-{
- info("checking request POST data");
-
- checkObject(aResponse, {
- postData: {
- text: {
- type: "longString",
- initial: /^Hello world! foobaz barr.+foobaz barrfo$/,
- length: 552,
- actor: /[a-z]/,
- },
- },
- postDataDiscarded: false,
- });
-
- is(aResponse.postData.text.initial.length,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH, "postData text initial length");
-
- onResponseHeaders = onResponseHeaders.bind(null, aState);
- aState.client.getResponseHeaders(aState.netActor,
- onResponseHeaders);
-}
-
-function onResponseHeaders(aState, aResponse)
-{
- info("checking response headers");
-
- ok(aResponse.headers.length > 0, "response headers > 0");
- ok(aResponse.headersSize > 0, "response headersSize > 0");
-
- checkHeadersOrCookies(aResponse.headers, {
- "Content-Type": /^application\/(json|octet-stream)$/,
- "Content-Length": /^\d+$/,
- "x-very-short": "hello world",
- "x-very-long": {
- "type": "longString",
- "length": 521,
- "initial": /^Lorem ipsum.+\. Donec vitae d$/,
- "actor": /[a-z]/,
- },
- });
-
- onResponseCookies = onResponseCookies.bind(null, aState);
- aState.client.getResponseCookies(aState.netActor,
- onResponseCookies);
-}
-
-function onResponseCookies(aState, aResponse)
-{
- info("checking response cookies");
-
- is(aResponse.cookies.length, 0, "response cookies length");
-
- onResponseContent = onResponseContent.bind(null, aState);
- aState.client.getResponseContent(aState.netActor,
- onResponseContent);
-}
-
-function onResponseContent(aState, aResponse)
-{
- info("checking response content");
-
- checkObject(aResponse, {
- content: {
- text: {
- type: "longString",
- initial: /^\{ id: "test JSON data"(.|\r|\n)+ barfoo ba$/g,
- length: 1070,
- actor: /[a-z]/,
- },
- },
- contentDiscarded: false,
- });
-
- is(aResponse.content.text.initial.length,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH, "content initial length");
-
- onEventTimings = onEventTimings.bind(null, aState);
- aState.client.getEventTimings(aState.netActor,
- onEventTimings);
-}
-
-function onEventTimings(aState, aResponse)
-{
- info("checking event timings");
-
- checkObject(aResponse, {
- timings: {
- blocked: /^-1|\d+$/,
- dns: /^-1|\d+$/,
- connect: /^-1|\d+$/,
- send: /^-1|\d+$/,
- wait: /^-1|\d+$/,
- receive: /^-1|\d+$/,
- },
- totalTime: /^\d+$/,
- });
-
- closeDebugger(aState, function() {
- DebuggerServer.LONG_STRING_LENGTH = ORIGINAL_LONG_STRING_LENGTH;
- DebuggerServer.LONG_STRING_INITIAL_LENGTH = ORIGINAL_LONG_STRING_INITIAL_LENGTH;
-
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_network_post.html b/toolkit/devtools/webconsole/test/test_network_post.html
deleted file mode 100644
index 73b08e6de..000000000
--- a/toolkit/devtools/webconsole/test/test_network_post.html
+++ /dev/null
@@ -1,286 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the network actor (POST request)</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the network actor (POST request)</p>
-
-<iframe src="http://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["NetworkActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- info("enable network request and response body logging");
-
- onSetPreferences = onSetPreferences.bind(null, aState);
- aState.client.setPreferences({
- "NetworkMonitor.saveRequestAndResponseBodies": true,
- }, onSetPreferences);
-}
-
-function onSetPreferences(aState, aResponse)
-{
- is(aResponse.updated.length, 1, "updated prefs length");
- is(aResponse.updated[0], "NetworkMonitor.saveRequestAndResponseBodies",
- "updated prefs length");
-
- info("test network POST request");
-
- onNetworkEvent = onNetworkEvent.bind(null, aState);
- aState.dbgClient.addListener("networkEvent", onNetworkEvent);
- onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
- aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
-
- let iframe = document.querySelector("iframe").contentWindow;
- iframe.wrappedJSObject.testXhrPost();
-}
-
-function onNetworkEvent(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "network event actor");
-
- info("checking the network event packet");
-
- let netActor = aPacket.eventActor;
-
- checkObject(netActor, {
- actor: /[a-z]/,
- startedDateTime: /^\d+\-\d+\-\d+T.+$/,
- url: /data\.json/,
- method: "POST",
- });
-
- aState.netActor = netActor.actor;
-
- aState.dbgClient.removeListener("networkEvent", onNetworkEvent);
-}
-
-let updates = [];
-
-function onNetworkEventUpdate(aState, aType, aPacket)
-{
- info("received networkEventUpdate " + aPacket.updateType);
- is(aPacket.from, aState.netActor, "networkEventUpdate actor");
-
- updates.push(aPacket.updateType);
-
- let expectedPacket = null;
-
- switch (aPacket.updateType) {
- case "requestHeaders":
- case "responseHeaders":
- ok(aPacket.headers > 0, "headers > 0");
- ok(aPacket.headersSize > 0, "headersSize > 0");
- break;
- case "requestCookies":
- expectedPacket = {
- cookies: 3,
- };
- break;
- case "requestPostData":
- ok(aPacket.dataSize > 0, "dataSize > 0");
- ok(!aPacket.discardRequestBody, "discardRequestBody");
- break;
- case "responseStart":
- expectedPacket = {
- response: {
- httpVersion: /^HTTP\/\d\.\d$/,
- status: 200,
- statusText: "OK",
- headersSize: /^\d+$/,
- discardResponseBody: false,
- },
- };
- break;
- case "securityInfo":
- expectedPacket = {
- state: "insecure",
- };
- break;
- case "responseCookies":
- expectedPacket = {
- cookies: 0,
- };
- break;
- case "responseContent":
- expectedPacket = {
- mimeType: "application/json",
- contentSize: /^\d+$/,
- discardResponseBody: false,
- };
- break;
- case "eventTimings":
- expectedPacket = {
- totalTime: /^\d+$/,
- };
- break;
- default:
- ok(false, "unknown network event update type: " +
- aPacket.updateType);
- return;
- }
-
- if (expectedPacket) {
- info("checking the packet content");
- checkObject(aPacket, expectedPacket);
- }
-
- if (updates.indexOf("responseContent") > -1 &&
- updates.indexOf("eventTimings") > -1) {
- aState.dbgClient.removeListener("networkEventUpdate",
- onNetworkEvent);
-
- onRequestHeaders = onRequestHeaders.bind(null, aState);
- aState.client.getRequestHeaders(aState.netActor,
- onRequestHeaders);
- }
-}
-
-function onRequestHeaders(aState, aResponse)
-{
- info("checking request headers");
-
- ok(aResponse.headers.length > 0, "request headers > 0");
- ok(aResponse.headersSize > 0, "request headersSize > 0");
- ok(!!aResponse.rawHeaders.length, "request rawHeaders available");
-
- checkHeadersOrCookies(aResponse.headers, {
- Referer: /network_requests_iframe\.html/,
- Cookie: /bug768096/,
- });
-
- checkRawHeaders(aResponse.rawHeaders, {
- Referer: /network_requests_iframe\.html/,
- Cookie: /bug768096/,
- });
-
- onRequestCookies = onRequestCookies.bind(null, aState);
- aState.client.getRequestCookies(aState.netActor,
- onRequestCookies);
-}
-
-function onRequestCookies(aState, aResponse)
-{
- info("checking request cookies");
-
- is(aResponse.cookies.length, 3, "request cookies length");
-
- checkHeadersOrCookies(aResponse.cookies, {
- foobar: "fooval",
- omgfoo: "bug768096",
- badcookie: "bug826798=st3fan",
- });
-
- onRequestPostData = onRequestPostData.bind(null, aState);
- aState.client.getRequestPostData(aState.netActor,
- onRequestPostData);
-}
-
-function onRequestPostData(aState, aResponse)
-{
- info("checking request POST data");
-
- checkObject(aResponse, {
- postData: {
- text: /^Hello world! foobaz barr.+foobaz barr$/,
- },
- postDataDiscarded: false,
- });
-
- is(aResponse.postData.text.length, 552, "postData text length");
-
- onResponseHeaders = onResponseHeaders.bind(null, aState);
- aState.client.getResponseHeaders(aState.netActor,
- onResponseHeaders);
-}
-
-function onResponseHeaders(aState, aResponse)
-{
- info("checking response headers");
-
- ok(aResponse.headers.length > 0, "response headers > 0");
- ok(aResponse.headersSize > 0, "response headersSize > 0");
- ok(!!aResponse.rawHeaders, "response rawHeaders available");
-
- checkHeadersOrCookies(aResponse.headers, {
- "Content-Type": /^application\/(json|octet-stream)$/,
- "Content-Length": /^\d+$/,
- });
-
- checkRawHeaders(aResponse.rawHeaders, {
- "Content-Type": /^application\/(json|octet-stream)$/,
- "Content-Length": /^\d+$/,
- });
-
- onResponseCookies = onResponseCookies.bind(null, aState);
- aState.client.getResponseCookies(aState.netActor,
- onResponseCookies);
-}
-
-function onResponseCookies(aState, aResponse)
-{
- info("checking response cookies");
-
- is(aResponse.cookies.length, 0, "response cookies length");
-
- onResponseContent = onResponseContent.bind(null, aState);
- aState.client.getResponseContent(aState.netActor,
- onResponseContent);
-}
-
-function onResponseContent(aState, aResponse)
-{
- info("checking response content");
-
- checkObject(aResponse, {
- content: {
- text: /"test JSON data"/,
- },
- contentDiscarded: false,
- });
-
- onEventTimings = onEventTimings.bind(null, aState);
- aState.client.getEventTimings(aState.netActor,
- onEventTimings);
-}
-
-function onEventTimings(aState, aResponse)
-{
- info("checking event timings");
-
- checkObject(aResponse, {
- timings: {
- blocked: /^-1|\d+$/,
- dns: /^-1|\d+$/,
- connect: /^-1|\d+$/,
- send: /^-1|\d+$/,
- wait: /^-1|\d+$/,
- receive: /^-1|\d+$/,
- },
- totalTime: /^\d+$/,
- });
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_network_security-hpkp.html b/toolkit/devtools/webconsole/test/test_network_security-hpkp.html
deleted file mode 100644
index a85d159bf..000000000
--- a/toolkit/devtools/webconsole/test/test_network_security-hpkp.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the network actor (HPKP detection)</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the network actor (HPKP detection)</p>
-
-<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let gCurrentTestCase = -1;
-const HPKP_PREF = "security.cert_pinning.process_headers_from_non_builtin_roots";
-
-// Static pins tested by unit/test_security-info-static-hpkp.js.
-const TEST_CASES = [
- {
- desc: "no Public Key Pinning",
- url: "https://example.com",
- usesPinning: false,
- },
- {
- desc: "dynamic Public Key Pinning with this request",
- url: "https://include-subdomains.pinning-dynamic.example.com/" +
- "browser/browser/base/content/test/general/pinning_headers.sjs",
- usesPinning: true,
- },
- {
- desc: "dynamic Public Key Pinning with previous request",
- url: "https://include-subdomains.pinning-dynamic.example.com/",
- usesPinning: true,
- }
-];
-
-function startTest()
-{
- // Need to enable this pref or pinning headers are rejected due test
- // certificate.
- Services.prefs.setBoolPref(HPKP_PREF, true);
- SimpleTest.registerCleanupFunction(() => {
- Services.prefs.setBoolPref(HPKP_PREF, false);
-
- // Reset pinning state.
- let gSSService = Cc["@mozilla.org/ssservice;1"]
- .getService(Ci.nsISiteSecurityService);
-
- let gIOService = Cc["@mozilla.org/network/io-service;1"]
- .getService(Ci.nsIIOService);
- for (let {url} of TEST_CASES) {
- let uri = gIOService.newURI(url, null, null);
- gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
- }
- });
-
- info("Test detection of Public Key Pinning.");
- removeEventListener("load", startTest);
- attachConsole(["NetworkActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
- aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
-
- runNextCase(aState);
-}
-
-function runNextCase(aState) {
- gCurrentTestCase++;
- if (gCurrentTestCase === TEST_CASES.length) {
- info("Tests ran. Cleaning up.");
- closeDebugger(aState, SimpleTest.finish);
- return;
- }
-
- let { desc, url } = TEST_CASES[gCurrentTestCase];
- info("Testing site with " + desc);
-
- let iframe = document.querySelector("iframe").contentWindow;
- iframe.wrappedJSObject.makeXhrCallback("GET", url);
-}
-
-function onNetworkEventUpdate(aState, aType, aPacket)
-{
- function onSecurityInfo(packet) {
- let data = TEST_CASES[gCurrentTestCase];
- is(packet.securityInfo.hpkp, data.usesPinning,
- "Public Key Pinning detected correctly.");
-
- runNextCase(aState);
- }
-
- if (aPacket.updateType === "securityInfo") {
- aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
- }
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_network_security-hsts.html b/toolkit/devtools/webconsole/test/test_network_security-hsts.html
deleted file mode 100644
index 1895c9113..000000000
--- a/toolkit/devtools/webconsole/test/test_network_security-hsts.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the network actor (HSTS detection)</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the network actor (HSTS detection)</p>
-
-<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let gCurrentTestCase = -1;
-const TEST_CASES = [
- {
- desc: "no HSTS",
- url: "https://example.com",
- usesHSTS: false,
- },
- {
- desc: "HSTS from this response",
- url: "https://example.com/"+
- "browser/browser/base/content/test/general/browser_star_hsts.sjs",
- usesHSTS: true,
- },
- {
- desc: "stored HSTS from previous response",
- url: "https://example.com/",
- usesHSTS: true,
- }
-];
-
-function startTest()
-{
-
- SimpleTest.registerCleanupFunction(() => {
- // Reset HSTS state.
- let gSSService = Cc["@mozilla.org/ssservice;1"]
- .getService(Ci.nsISiteSecurityService);
-
- let gIOService = Cc["@mozilla.org/network/io-service;1"]
- .getService(Ci.nsIIOService);
-
- let uri = gIOService.newURI(TEST_CASES[0].url, null, null);
- gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
- });
-
- info("Test detection of HTTP Strict Transport Security.");
- removeEventListener("load", startTest);
- attachConsole(["NetworkActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
- aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
-
- runNextCase(aState);
-}
-
-function runNextCase(aState) {
- gCurrentTestCase++;
- if (gCurrentTestCase === TEST_CASES.length) {
- info("Tests ran. Cleaning up.");
- closeDebugger(aState, SimpleTest.finish);
- return;
- }
-
- let { desc, url } = TEST_CASES[gCurrentTestCase];
- info("Testing site with " + desc);
-
- let iframe = document.querySelector("iframe").contentWindow;
- iframe.wrappedJSObject.makeXhrCallback("GET", url);
-}
-
-function onNetworkEventUpdate(aState, aType, aPacket)
-{
- function onSecurityInfo(packet) {
- let data = TEST_CASES[gCurrentTestCase];
- is(packet.securityInfo.hsts, data.usesHSTS,
- "Strict Transport Security detected correctly.");
-
- runNextCase(aState);
- }
-
- if (aPacket.updateType === "securityInfo") {
- aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
- }
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_nsiconsolemessage.html b/toolkit/devtools/webconsole/test/test_nsiconsolemessage.html
deleted file mode 100644
index ef8b8067e..000000000
--- a/toolkit/devtools/webconsole/test/test_nsiconsolemessage.html
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for nsIConsoleMessages</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Make sure that nsIConsoleMessages are logged. See bug 859756.</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-"use strict";
-SimpleTest.waitForExplicitFinish();
-
-let expectedMessages = [];
-
-function startTest()
-{
- removeEventListener("load", startTest);
- attachConsole(["PageError"], onAttach);
-}
-
-function onAttach(aState, aResponse)
-{
- onLogMessage = onLogMessage.bind(null, aState);
- aState.dbgClient.addListener("logMessage", onLogMessage);
-
- expectedMessages = [{
- message: "hello world! bug859756",
- timeStamp: /^\d+$/,
- }];
-
- Services.console.logStringMessage("hello world! bug859756");
-
- info("waiting for messages");
-}
-
-let receivedMessages = [];
-
-function onLogMessage(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "packet actor");
- info("received message: " + aPacket.message);
-
- let found = false;
- for (let expected of expectedMessages) {
- if (expected.message == aPacket.message) {
- found = true;
- break;
- }
- }
- if (!found) {
- return;
- }
-
- receivedMessages.push(aPacket);
- if (receivedMessages.length != expectedMessages.length) {
- return;
- }
-
- aState.dbgClient.removeListener("logMessage", onLogMessage);
-
- checkObject(receivedMessages, expectedMessages);
-
- closeDebugger(aState, () => SimpleTest.finish());
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_object_actor.html b/toolkit/devtools/webconsole/test/test_object_actor.html
deleted file mode 100644
index 5eae48b66..000000000
--- a/toolkit/devtools/webconsole/test/test_object_actor.html
+++ /dev/null
@@ -1,178 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the object actor</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the object actor</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let expectedProps = [];
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["ConsoleAPI"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onConsoleCall = onConsoleCall.bind(null, aState);
- aState.dbgClient.addListener("consoleAPICall", onConsoleCall);
-
- let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 3)).join("\u0629");
-
- // Here we put the objects in the correct window, to avoid having them all
- // wrapped by proxies for cross-compartment access.
-
- let foobarObject = top.Object.create(null);
- foobarObject.tamarbuta = longString;
- foobarObject.foo = 1;
- foobarObject.foobar = "hello";
- foobarObject.omg = null;
- foobarObject.testfoo = false;
- foobarObject.notInspectable = top.Object.create(null);
- foobarObject.omgfn = new top.Function("return 'myResult'");
- foobarObject.abArray = new top.Array("a", "b");
- foobarObject.foobaz = top.document;
-
- top.Object.defineProperty(foobarObject, "getterAndSetter", {
- enumerable: true,
- get: new top.Function("return 'foo';"),
- set: new top.Function("1+2"),
- });
-
- foobarObject.longStringObj = top.Object.create(null);
- foobarObject.longStringObj.toSource = new top.Function("'" + longString + "'");
- foobarObject.longStringObj.toString = new top.Function("'" + longString + "'");
- foobarObject.longStringObj.boom = "explode";
-
- top.wrappedJSObject.foobarObject = foobarObject;
- top.console.log("hello", top.wrappedJSObject.foobarObject);
-
- expectedProps = {
- "abArray": {
- value: {
- type: "object",
- class: "Array",
- actor: /[a-z]/,
- },
- },
- "foo": {
- configurable: true,
- enumerable: true,
- writable: true,
- value: 1,
- },
- "foobar": {
- value: "hello",
- },
- "foobaz": {
- value: {
- type: "object",
- class: "XULDocument",
- actor: /[a-z]/,
- },
- },
- "getterAndSetter": {
- get: {
- type: "object",
- class: "Function",
- actor: /[a-z]/,
- },
- set: {
- type: "object",
- class: "Function",
- actor: /[a-z]/,
- },
- },
- "longStringObj": {
- value: {
- type: "object",
- class: "Object",
- actor: /[a-z]/,
- },
- },
- "notInspectable": {
- value: {
- type: "object",
- class: "Object",
- actor: /[a-z]/,
- },
- },
- "omg": {
- value: { type: "null" },
- },
- "omgfn": {
- value: {
- type: "object",
- class: "Function",
- actor: /[a-z]/,
- },
- },
- "tamarbuta": {
- value: {
- type: "longString",
- initial: longString.substring(0,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH),
- length: longString.length,
- },
- },
- "testfoo": {
- value: false,
- },
- };
-}
-
-function onConsoleCall(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "console API call actor");
-
- info("checking the console API call packet");
-
- checkConsoleAPICall(aPacket.message, {
- level: "log",
- filename: /test_object_actor/,
- functionName: "onAttach",
- arguments: ["hello", {
- type: "object",
- actor: /[a-z]/,
- }],
- });
-
- aState.dbgClient.removeListener("consoleAPICall", onConsoleCall);
-
- info("inspecting object properties");
- let args = aPacket.message.arguments;
- onProperties = onProperties.bind(null, aState);
-
- let client = new ObjectClient(aState.dbgClient, args[1]);
- client.getPrototypeAndProperties(onProperties);
-}
-
-function onProperties(aState, aResponse)
-{
- let props = aResponse.ownProperties;
- is(Object.keys(props).length, Object.keys(expectedProps).length,
- "number of enumerable properties");
- checkObject(props, expectedProps);
-
- expectedProps = [];
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_object_actor_native_getters.html b/toolkit/devtools/webconsole/test/test_object_actor_native_getters.html
deleted file mode 100644
index c7bda2597..000000000
--- a/toolkit/devtools/webconsole/test/test_object_actor_native_getters.html
+++ /dev/null
@@ -1,106 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the native getters in object actors</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the native getters in object actors</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let expectedProps = [];
-let expectedSafeGetters = [];
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["ConsoleAPI"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onConsoleCall = onConsoleCall.bind(null, aState);
- aState.dbgClient.addListener("consoleAPICall", onConsoleCall);
-
- top.console.log("hello", document);
-
- expectedProps = {
- "location": {
- get: {
- type: "object",
- class: "Function",
- actor: /[a-z]/,
- },
- },
- };
-
- expectedSafeGetters = {
- "title": {
- getterValue: /native getters in object actors/,
- getterPrototypeLevel: 2,
- },
- "styleSheets": {
- getterValue: "[object Object]",
- getterPrototypeLevel: 2,
- },
- };
-}
-
-function onConsoleCall(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "console API call actor");
-
- info("checking the console API call packet");
-
- checkConsoleAPICall(aPacket.message, {
- level: "log",
- filename: /test_object_actor/,
- functionName: "onAttach",
- arguments: ["hello", {
- type: "object",
- actor: /[a-z]/,
- }],
- });
-
- aState.dbgClient.removeListener("consoleAPICall", onConsoleCall);
-
- info("inspecting object properties");
- let args = aPacket.message.arguments;
- onProperties = onProperties.bind(null, aState);
-
- let client = new ObjectClient(aState.dbgClient, args[1]);
- client.getPrototypeAndProperties(onProperties);
-}
-
-function onProperties(aState, aResponse)
-{
- let props = aResponse.ownProperties;
- let keys = Object.keys(props);
- info(keys.length + " ownProperties: " + keys);
-
- ok(keys.length >= Object.keys(expectedProps).length, "number of properties");
-
- info("check ownProperties");
- checkObject(props, expectedProps);
- info("check safeGetterValues");
- checkObject(aResponse.safeGetterValues, expectedSafeGetters);
-
- expectedProps = [];
- expectedSafeGetters = [];
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_object_actor_native_getters_lenient_this.html b/toolkit/devtools/webconsole/test/test_object_actor_native_getters_lenient_this.html
deleted file mode 100644
index 186e35b73..000000000
--- a/toolkit/devtools/webconsole/test/test_object_actor_native_getters_lenient_this.html
+++ /dev/null
@@ -1,79 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test that WebIDL attributes with the LenientThis extended attribute
- do not appear in the wrong objects</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for the native getters in object actors</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["ConsoleAPI"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- onConsoleCall = onConsoleCall.bind(null, aState);
- aState.dbgClient.addListener("consoleAPICall", onConsoleCall);
-
- let docAsProto = Object.create(document);
-
- top.console.log("hello", docAsProto);
-}
-
-function onConsoleCall(aState, aType, aPacket)
-{
- is(aPacket.from, aState.actor, "console API call actor");
-
- info("checking the console API call packet");
-
- checkConsoleAPICall(aPacket.message, {
- level: "log",
- filename: /test_object_actor/,
- functionName: "onAttach",
- arguments: ["hello", {
- type: "object",
- actor: /[a-z]/,
- }],
- });
-
- aState.dbgClient.removeListener("consoleAPICall", onConsoleCall);
-
- info("inspecting object properties");
- let args = aPacket.message.arguments;
- onProperties = onProperties.bind(null, aState);
-
- let client = new ObjectClient(aState.dbgClient, args[1]);
- client.getPrototypeAndProperties(onProperties);
-}
-
-function onProperties(aState, aResponse)
-{
- let props = aResponse.ownProperties;
- let keys = Object.keys(props);
- info(keys.length + " ownProperties: " + keys);
-
- is(keys.length, 0, "number of properties");
- keys = Object.keys(aResponse.safeGetterValues);
- is(keys.length, 0, "number of safe getters");
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_page_errors.html b/toolkit/devtools/webconsole/test/test_page_errors.html
deleted file mode 100644
index 7a924667f..000000000
--- a/toolkit/devtools/webconsole/test/test_page_errors.html
+++ /dev/null
@@ -1,102 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for page errors</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for page errors</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let expectedPageErrors = [];
-
-function doPageErrors()
-{
- expectedPageErrors = [
- {
- errorMessage: /fooColor/,
- sourceName: /test_page_errors/,
- category: "CSS Parser",
- timeStamp: /^\d+$/,
- error: false,
- warning: true,
- exception: false,
- strict: false,
- },
- {
- errorMessage: /doTheImpossible/,
- sourceName: /test_page_errors/,
- category: "chrome javascript",
- timeStamp: /^\d+$/,
- error: false,
- warning: false,
- exception: true,
- strict: false,
- },
- ];
-
- let container = document.createElement("script");
- document.body.appendChild(container);
- container.textContent = "document.body.style.color = 'fooColor';";
- document.body.removeChild(container);
-
- SimpleTest.expectUncaughtException();
-
- container = document.createElement("script");
- document.body.appendChild(container);
- container.textContent = "document.doTheImpossible();";
- document.body.removeChild(container);
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
-
- attachConsole(["PageError"], onAttach);
-}
-
-function onAttach(aState, aResponse)
-{
- onPageError = onPageError.bind(null, aState);
- aState.dbgClient.addListener("pageError", onPageError);
- doPageErrors();
-}
-
-let pageErrors = [];
-
-function onPageError(aState, aType, aPacket)
-{
- if (!aPacket.pageError.sourceName.includes("test_page_errors")) {
- info("Ignoring error from unknown source: " + aPacket.pageError.sourceName);
- return;
- }
-
- is(aPacket.from, aState.actor, "page error actor");
-
- pageErrors.push(aPacket.pageError);
- if (pageErrors.length != expectedPageErrors.length) {
- return;
- }
-
- aState.dbgClient.removeListener("pageError", onPageError);
-
- expectedPageErrors.forEach(function(aMessage, aIndex) {
- info("checking received page error #" + aIndex);
- checkObject(pageErrors[aIndex], expectedPageErrors[aIndex]);
- });
-
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_reflow.html b/toolkit/devtools/webconsole/test/test_reflow.html
deleted file mode 100644
index c010604be..000000000
--- a/toolkit/devtools/webconsole/test/test_reflow.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Test for the Reflow Activity</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Test for reflow events</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-let client;
-
-function generateReflow()
-{
- top.document.documentElement.style.display = "none";
- top.document.documentElement.getBoundingClientRect();
- top.document.documentElement.style.display = "block";
-}
-
-function startTest()
-{
- removeEventListener("load", startTest);
- attachConsole(["ReflowActivity"], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- client = aState.dbgClient;
-
- onReflowActivity = onReflowActivity.bind(null, aState);
- client.addListener("reflowActivity", onReflowActivity);
- generateReflow();
-}
-
-// We are expecting 3 reflow events.
-let expectedEvents = [
- {
- interruptible: false,
- sourceURL: "chrome://mochitests/content/chrome/toolkit/devtools/webconsole/test/test_reflow.html",
- functionName: "generateReflow"
- },
- {
- interruptible: true,
- sourceURL: null,
- functionName: null
- },
- {
- interruptible: true,
- sourceURL: null,
- functionName: null
- },
-];
-
-let receivedEvents = [];
-
-
-function onReflowActivity(aState, aType, aPacket)
-{
- info("packet: " + aPacket.message);
- receivedEvents.push(aPacket);
- if (receivedEvents.length == expectedEvents.length) {
- checkEvents();
- finish(aState);
- }
-}
-
-function checkEvents() {
- for (let i = 0; i < expectedEvents.length; i++) {
- let a = expectedEvents[i];
- let b = receivedEvents[i];
- for (let key in a) {
- is(a[key], b[key], "field " + key + " is valid");
- }
- }
-}
-
-function finish(aState) {
- client.removeListener("reflowActivity", onReflowActivity);
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/test_throw.html b/toolkit/devtools/webconsole/test/test_throw.html
deleted file mode 100644
index bf09d558a..000000000
--- a/toolkit/devtools/webconsole/test/test_throw.html
+++ /dev/null
@@ -1,78 +0,0 @@
-<!DOCTYPE HTML>
-<html lang="en">
-<head>
- <meta charset="utf8">
- <title>Web Console throw tests</title>
- <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript;version=1.8" src="common.js"></script>
- <!-- Any copyright is dedicated to the Public Domain.
- - http://creativecommons.org/publicdomain/zero/1.0/ -->
-</head>
-<body>
-<p>Web Console throw tests</p>
-
-<script class="testbody" type="text/javascript;version=1.8">
-SimpleTest.waitForExplicitFinish();
-
-function startTest()
-{
- removeEventListener("load", startTest);
- attachConsole([], onAttach, true);
-}
-
-function onAttach(aState, aResponse)
-{
- let tests = [];
-
- let falsyValues = ["-0", "null", "undefined", "Infinity", "-Infinity", "NaN"];
- falsyValues.forEach(function(value) {
- tests.push(function() {
- aState.client.evaluateJS("throw " + value + ";", function(aResponse) {
- let type = aResponse.exception.type;
- is(type, value, "exception.type for throw " + value);
- nextTest();
- });
- });
- });
-
- let identityTestValues = [false, 0];
- identityTestValues.forEach(function(value) {
- tests.push(function() {
- aState.client.evaluateJS("throw " + value + ";", function(aResponse) {
- let exception = aResponse.exception;
- ise(exception, value, "response.exception for throw " + value);
- nextTest();
- });
- });
- });
-
- let longString = Array(DebuggerServer.LONG_STRING_LENGTH + 1).join("a"),
- shortedString = longString.substring(0,
- DebuggerServer.LONG_STRING_INITIAL_LENGTH
- );
- tests.push(function() {
- aState.client.evaluateJS("throw '" + longString + "';", function(aResponse) {
- is(aResponse.exception.initial, shortedString,
- "exception.initial for throw longString"
- );
- is(aResponse.exceptionMessage.initial, shortedString,
- "exceptionMessage.initial for throw longString"
- );
- nextTest();
- });
- });
-
- runTests(tests, endTest.bind(null, aState));
-}
-
-function endTest(aState)
-{
- closeDebugger(aState, function() {
- SimpleTest.finish();
- });
-}
-
-addEventListener("load", startTest);
-</script>
-</body>
-</html>
diff --git a/toolkit/devtools/webconsole/test/unit/test_js_property_provider.js b/toolkit/devtools/webconsole/test/unit/test_js_property_provider.js
deleted file mode 100644
index 0945e4e9e..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_js_property_provider.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
-// Any copyright is dedicated to the Public Domain.
-// http://creativecommons.org/publicdomain/zero/1.0/
-
-"use strict";
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-let JSPropertyProvider = devtools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider;
-
-Components.utils.import("resource://gre/modules/jsdebugger.jsm");
-addDebuggerToGlobal(this);
-
-function run_test() {
- const testArray = 'var testArray = [\
- {propA: "A"},\
- {\
- propB: "B", \
- propC: [\
- {propD: "D"}\
- ]\
- },\
- [\
- {propE: "E"}\
- ]\
- ];'
-
- const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
-
- let sandbox = Components.utils.Sandbox("http://example.com");
- let dbg = new Debugger;
- let dbgObject = dbg.addDebuggee(sandbox);
- Components.utils.evalInSandbox(testArray, sandbox);
- Components.utils.evalInSandbox(testObject, sandbox);
-
- let results = JSPropertyProvider(dbgObject, null, "testArray[0].");
- do_print("Test that suggestions are given for 'foo[n]' where n is an integer.");
- test_has_result(results, "propA");
-
- do_print("Test that suggestions are given for multidimensional arrays.");
- results = JSPropertyProvider(dbgObject, null, "testArray[2][0].");
- test_has_result(results, "propE");
-
- do_print("Test that suggestions are not given for index that's out of bounds.");
- results = JSPropertyProvider(dbgObject, null, "testArray[10].");
- do_check_null(results);
-
- do_print("Test that no suggestions are given if an index is not numerical somewhere in the chain.");
- results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'][0].");
- do_check_null(results);
-
- results = JSPropertyProvider(dbgObject, null, "testObject['propA'][0].");
- do_check_null(results);
-
- results = JSPropertyProvider(dbgObject, null, "testArray[0]['propC'].");
- do_check_null(results);
-
- results = JSPropertyProvider(dbgObject, null, "testArray[][1].");
- do_check_null(results);
-}
-
-/**
- * A helper that ensures (required) results were found.
- * @param Object aResults
- * The results returned by JSPropertyProvider.
- * @param String aRequiredSuggestion
- * A suggestion that must be found from the results.
- */
-function test_has_result(aResults, aRequiredSuggestion) {
- do_check_neq(aResults, null);
- do_check_true(aResults.matches.length > 0);
- do_check_true(aResults.matches.indexOf(aRequiredSuggestion) !== -1);
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_network_helper.js b/toolkit/devtools/webconsole/test/unit/test_network_helper.js
deleted file mode 100644
index 2d8db570c..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_network_helper.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-const Cu = Components.utils;
-const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-function run_test() {
- test_isTextMimeType();
-}
-
-function test_isTextMimeType () {
- do_check_eq(NetworkHelper.isTextMimeType("text/plain"), true);
- do_check_eq(NetworkHelper.isTextMimeType("application/javascript"), true);
- do_check_eq(NetworkHelper.isTextMimeType("application/json"), true);
- do_check_eq(NetworkHelper.isTextMimeType("text/css"), true);
- do_check_eq(NetworkHelper.isTextMimeType("text/html"), true);
- do_check_eq(NetworkHelper.isTextMimeType("image/svg+xml"), true);
- do_check_eq(NetworkHelper.isTextMimeType("application/xml"), true);
-
- // Test custom JSON subtype
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+json"), true);
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-json"), true);
- // Test custom XML subtype
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+xml"), true);
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-xml"), false);
- // Test case-insensitive
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.BIG-CORP+json"), true);
- // Test non-text type
- do_check_eq(NetworkHelper.isTextMimeType("image/png"), false);
- // Test invalid types
- do_check_eq(NetworkHelper.isTextMimeType("application/foo-+json"), false);
- do_check_eq(NetworkHelper.isTextMimeType("application/-foo+json"), false);
- do_check_eq(NetworkHelper.isTextMimeType("application/foo--bar+json"), false);
-
- // Test we do not cause internal errors with unoptimized regex. Bug 961097
- do_check_eq(NetworkHelper.isTextMimeType("application/vnd.google.safebrowsing-chunk"), false);
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js b/toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js
deleted file mode 100644
index 63b95a10c..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* 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";
-
-// Tests that NetworkHelper.parseCertificateInfo parses certificate information
-// correctly.
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const DUMMY_CERT = {
- commonName: "cn",
- organization: "o",
- organizationalUnit: "ou",
- issuerCommonName: "issuerCN",
- issuerOrganization: "issuerO",
- issuerOrganizationUnit: "issuerOU",
- sha256Fingerprint: "qwertyuiopoiuytrewq",
- sha1Fingerprint: "qwertyuiop",
- validity: {
- notBeforeLocalDay: "yesterday",
- notAfterLocalDay: "tomorrow",
- }
-};
-
-function run_test() {
- do_print("Testing NetworkHelper.parseCertificateInfo.");
-
- let result = NetworkHelper.parseCertificateInfo(DUMMY_CERT);
-
- // Subject
- equal(result.subject.commonName, DUMMY_CERT.commonName,
- "Common name is correct.");
- equal(result.subject.organization, DUMMY_CERT.organization,
- "Organization is correct.");
- equal(result.subject.organizationalUnit, DUMMY_CERT.organizationalUnit,
- "Organizational unit is correct.");
-
- // Issuer
- equal(result.issuer.commonName, DUMMY_CERT.issuerCommonName,
- "Common name of the issuer is correct.");
- equal(result.issuer.organization, DUMMY_CERT.issuerOrganization,
- "Organization of the issuer is correct.");
- equal(result.issuer.organizationalUnit, DUMMY_CERT.issuerOrganizationalUnit,
- "Organizational unit of the issuer is correct.");
-
- // Validity
- equal(result.validity.start, DUMMY_CERT.validity.notBeforeLocalDay,
- "Start of the validity period is correct.");
- equal(result.validity.end, DUMMY_CERT.validity.notAfterLocalDay,
- "End of the validity period is correct.");
-
- // Fingerprints
- equal(result.fingerprint.sha1, DUMMY_CERT.sha1Fingerprint,
- "Certificate SHA1 fingerprint is correct.");
- equal(result.fingerprint.sha256, DUMMY_CERT.sha256Fingerprint,
- "Certificate SHA256 fingerprint is correct.");
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-parser.js b/toolkit/devtools/webconsole/test/unit/test_security-info-parser.js
deleted file mode 100644
index 379d3b0e5..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-parser.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* 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 NetworkHelper.parseSecurityInfo returns correctly formatted object.
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const wpl = Ci.nsIWebProgressListener;
-const MockCertificate = {
- commonName: "cn",
- organization: "o",
- organizationalUnit: "ou",
- issuerCommonName: "issuerCN",
- issuerOrganization: "issuerO",
- issuerOrganizationUnit: "issuerOU",
- sha256Fingerprint: "qwertyuiopoiuytrewq",
- sha1Fingerprint: "qwertyuiop",
- validity: {
- notBeforeLocalDay: "yesterday",
- notAfterLocalDay: "tomorrow",
- }
-};
-
-const MockSecurityInfo = {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
- Ci.nsISSLStatusProvider]),
- securityState: wpl.STATE_IS_SECURE,
- errorCode: 0,
- SSLStatus: {
- cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
- protocolVersion: 3, // TLS_VERSION_1_2
- serverCert: MockCertificate,
- }
-};
-
-function run_test() {
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
-
- equal(result.state, "secure", "State is correct.");
-
- equal(result.cipherSuite, MockSecurityInfo.cipherSuite,
- "Cipher suite is correct.");
-
- equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct.");
-
- deepEqual(result.cert, NetworkHelper.parseCertificateInfo(MockCertificate),
- "Certificate information is correct.");
-
- equal(result.hpkp, false, "HPKP is false when URI is not available.");
- equal(result.hsts, false, "HSTS is false when URI is not available.");
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js b/toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js
deleted file mode 100644
index 9283e4ddb..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* 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";
-
-// Tests that NetworkHelper.formatSecurityProtocol returns correct
-// protocol version strings.
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const TEST_CASES = [
- {
- description: "SSL_VERSION_3",
- input: 0,
- expected: "SSLv3"
- }, {
- description: "TLS_VERSION_1",
- input: 1,
- expected: "TLSv1"
- }, {
- description: "TLS_VERSION_1.1",
- input: 2,
- expected: "TLSv1.1"
- }, {
- description: "TLS_VERSION_1.2",
- input: 3,
- expected: "TLSv1.2"
- }, {
- description: "invalid version",
- input: -1,
- expected: "Unknown"
- },
-];
-
-function run_test() {
- do_print("Testing NetworkHelper.formatSecurityProtocol.");
-
- for (let {description, input, expected} of TEST_CASES) {
- do_print("Testing " + description);
-
- equal(NetworkHelper.formatSecurityProtocol(input), expected,
- "Got the expected protocol string.");
- }
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-state.js b/toolkit/devtools/webconsole/test/unit/test_security-info-state.js
deleted file mode 100644
index 43d4ed4d5..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-state.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/* 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";
-
-// Tests that security info parser gives correct general security state for
-// different cases.
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const wpl = Ci.nsIWebProgressListener;
-const MockSecurityInfo = {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
- Ci.nsISSLStatusProvider]),
- securityState: wpl.STATE_IS_BROKEN,
- errorCode: 0,
- SSLStatus: {
- protocolVersion: 3, // nsISSLStatus.TLS_VERSION_1_2
- cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
- }
-};
-
-function run_test() {
- test_nullSecurityInfo();
- test_insecureSecurityInfoWithNSSError();
- test_insecureSecurityInfoWithoutNSSError();
- test_brokenSecurityInfo();
- test_secureSecurityInfo();
-}
-
-/**
- * Test that undefined security information is returns "insecure".
- */
-function test_nullSecurityInfo() {
- let result = NetworkHelper.parseSecurityInfo(null, {});
- equal(result.state, "insecure",
- "state == 'insecure' when securityInfo was undefined");
-}
-
-/**
- * Test that STATE_IS_INSECURE with NSSError returns "broken"
- */
-function test_insecureSecurityInfoWithNSSError() {
- MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
-
- // Taken from security/manager/ssl/tests/unit/head_psm.js.
- MockSecurityInfo.errorCode = -8180;
-
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
- equal(result.state, "broken",
- "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " +
- "errorCode is NSS error.");
-
- MockSecurityInfo.errorCode = 0;
-}
-
-/**
- * Test that STATE_IS_INSECURE without NSSError returns "insecure"
- */
-function test_insecureSecurityInfoWithoutNSSError() {
- MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
-
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
- equal(result.state, "insecure",
- "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " +
- "errorCode is not NSS error.");
-}
-
-/**
- * Test that STATE_IS_SECURE returns "secure"
- */
-function test_secureSecurityInfo() {
- MockSecurityInfo.securityState = wpl.STATE_IS_SECURE;
-
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
- equal(result.state, "secure",
- "state == 'secure' if securityState contains STATE_IS_SECURE flag");
-}
-
-/**
- * Test that STATE_IS_BROKEN returns "weak"
- */
-function test_brokenSecurityInfo() {
- MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN;
-
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
- equal(result.state, "weak",
- "state == 'weak' if securityState contains STATE_IS_BROKEN flag");
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-static-hpkp.js b/toolkit/devtools/webconsole/test/unit/test_security-info-static-hpkp.js
deleted file mode 100644
index 8dfb64daa..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-static-hpkp.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* 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 NetworkHelper.parseSecurityInfo correctly detects static hpkp pins
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/Services.jsm");
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const wpl = Ci.nsIWebProgressListener;
-
-const MockSecurityInfo = {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
- Ci.nsISSLStatusProvider]),
- securityState: wpl.STATE_IS_SECURE,
- errorCode: 0,
- SSLStatus: {
- cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
- protocolVersion: 3, // TLS_VERSION_1_2
- serverCert: {
- validity: {}
- },
- }
-};
-
-const MockHttpInfo = {
- hostname: "include-subdomains.pinning.example.com",
- private: false,
-};
-
-function run_test() {
- Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 1);
- let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, MockHttpInfo);
- equal(result.hpkp, true, "Static HPKP detected.");
-}
diff --git a/toolkit/devtools/webconsole/test/unit/test_security-info-weakness-reasons.js b/toolkit/devtools/webconsole/test/unit/test_security-info-weakness-reasons.js
deleted file mode 100644
index 7c315c494..000000000
--- a/toolkit/devtools/webconsole/test/unit/test_security-info-weakness-reasons.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/* 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";
-
-// Tests that NetworkHelper.getReasonsForWeakness returns correct reasons for
-// weak requests.
-
-const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
-
-Object.defineProperty(this, "NetworkHelper", {
- get: function() {
- return devtools.require("devtools/toolkit/webconsole/network-helper");
- },
- configurable: true,
- writeable: false,
- enumerable: true
-});
-
-const Ci = Components.interfaces;
-const wpl = Ci.nsIWebProgressListener;
-const TEST_CASES = [
- {
- description: "weak cipher",
- input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_WEAK_CRYPTO,
- expected: ["cipher"]
- }, {
- description: "weak sslv3 protocol",
- input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_SSL_3,
- expected: ["sslv3"]
- }, {
- description: "weak cipher + sslv3",
- input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_WEAK_CRYPTO | wpl.STATE_USES_SSL_3,
- expected: ["sslv3", "cipher"] // order matters for deepEqual
- }, {
- description: "only STATE_IS_BROKEN flag",
- input: wpl.STATE_IS_BROKEN,
- expected: []
- }, {
- description: "only STATE_IS_SECURE flag",
- input: wpl.STATE_IS_SECURE,
- expected: []
- },
-];
-
-function run_test() {
- do_print("Testing NetworkHelper.getReasonsForWeakness.");
-
- for (let {description, input, expected} of TEST_CASES) {
- do_print("Testing " + description);
-
- deepEqual(NetworkHelper.getReasonsForWeakness(input), expected,
- "Got the expected reasons for weakness.");
- }
-}
diff --git a/toolkit/devtools/webconsole/test/unit/xpcshell.ini b/toolkit/devtools/webconsole/test/unit/xpcshell.ini
deleted file mode 100644
index 44de2511f..000000000
--- a/toolkit/devtools/webconsole/test/unit/xpcshell.ini
+++ /dev/null
@@ -1,14 +0,0 @@
-[DEFAULT]
-head =
-tail =
-skip-if = toolkit == 'android' || toolkit == 'gonk'
-support-files =
-
-[test_js_property_provider.js]
-[test_network_helper.js]
-[test_security-info-certificate.js]
-[test_security-info-parser.js]
-[test_security-info-protocol-version.js]
-[test_security-info-state.js]
-[test_security-info-static-hpkp.js]
-[test_security-info-weakness-reasons.js]
diff --git a/toolkit/devtools/webconsole/webconsole.js b/toolkit/devtools/webconsole/webconsole.js
new file mode 100644
index 000000000..3c7d022b0
--- /dev/null
+++ b/toolkit/devtools/webconsole/webconsole.js
@@ -0,0 +1,5456 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
+loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
+loader.lazyGetter(this, "AutocompletePopup",
+ () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
+loader.lazyGetter(this, "ToolSidebar",
+ () => require("devtools/framework/sidebar").ToolSidebar);
+loader.lazyGetter(this, "NetworkPanel",
+ () => require("devtools/webconsole/network-panel").NetworkPanel);
+loader.lazyGetter(this, "ConsoleOutput",
+ () => require("devtools/webconsole/console-output").ConsoleOutput);
+loader.lazyGetter(this, "Messages",
+ () => require("devtools/webconsole/console-output").Messages);
+loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm");
+loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
+loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
+loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
+loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
+
+const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
+
+const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
+
+const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm";
+
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
+
+const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
+
+const CONSOLE_DIR_VIEW_HEIGHT = 0.6;
+
+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 = 200;
+
+// 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;
+const CATEGORY_INPUT = 4; // always on
+const CATEGORY_OUTPUT = 5; // always on
+const CATEGORY_SECURITY = 6;
+
+// 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",
+];
+
+// 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).
+//
+// Most of these rather idiosyncratic names are historical and predate the
+// division of message type into "category" and "severity".
+const MESSAGE_PREFERENCE_KEYS = [
+// Error Warning Info Log
+ [ "network", "netwarn", "netxhr", "networkinfo", ], // Network
+ [ "csserror", "cssparser", null, "csslog", ], // CSS
+ [ "exception", "jswarn", null, "jslog", ], // JS
+ [ "error", "warn", "info", "log", ], // Web Developer
+ [ null, null, null, null, ], // Input
+ [ null, null, null, null, ], // Output
+ [ "secerror", "secwarn", null, null, ], // Security
+];
+
+// A mapping from the console API log event levels to the Web Console
+// severities.
+const LEVELS = {
+ error: SEVERITY_ERROR,
+ exception: SEVERITY_ERROR,
+ assert: SEVERITY_ERROR,
+ warn: SEVERITY_WARNING,
+ info: SEVERITY_INFO,
+ log: SEVERITY_LOG,
+ trace: SEVERITY_LOG,
+ table: SEVERITY_LOG,
+ debug: SEVERITY_LOG,
+ dir: SEVERITY_LOG,
+ group: SEVERITY_LOG,
+ groupCollapsed: SEVERITY_LOG,
+ groupEnd: SEVERITY_LOG,
+ time: SEVERITY_LOG,
+ timeEnd: SEVERITY_LOG,
+ count: SEVERITY_LOG
+};
+
+// 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;
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+// 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 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; // milliseconds
+
+// The maximum amount of time that can be spent doing cleanup inside of the
+// flush output callback. If things don't get cleaned up in this time,
+// then it will start again the next time it is called.
+const MAX_CLEANUP_TIME = 10; // milliseconds
+
+// When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
+// output updates to this number of milliseconds. So during a lot of output we
+// update every N milliseconds given here.
+const THROTTLE_UPDATES = 1000; // milliseconds
+
+// 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";
+
+/**
+ * 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 aWebConsoleOwner
+ * The WebConsole owner object.
+ */
+function WebConsoleFrame(aWebConsoleOwner)
+{
+ this.owner = aWebConsoleOwner;
+ this.hudId = this.owner.hudId;
+ this.window = this.owner.iframeWindow;
+
+ this._repeatNodes = {};
+ this._outputQueue = [];
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this._networkRequests = {};
+ this.filterPrefs = {};
+
+ this.output = new ConsoleOutput(this);
+
+ this._toggleFilter = this._toggleFilter.bind(this);
+ this._onPanelSelected = this._onPanelSelected.bind(this);
+ this._flushMessageQueue = this._flushMessageQueue.bind(this);
+ this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+
+ this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._outputTimerInitialized = false;
+
+ EventEmitter.decorate(this);
+}
+exports.WebConsoleFrame = WebConsoleFrame;
+
+WebConsoleFrame.prototype = {
+ /**
+ * The WebConsole instance that owns this frame.
+ * @see hudservice.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() this.owner.mainPopupSet,
+
+ /**
+ * Holds the initialization promise object.
+ * @private
+ * @type object
+ */
+ _initDefer: null,
+
+ /**
+ * Holds the network requests currently displayed by the Web Console. Each key
+ * represents the connection ID and the value is network request information.
+ * @private
+ * @type object
+ */
+ _networkRequests: 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() this.proxy ? this.proxy.webConsoleClient : null,
+
+ _destroyer: null,
+
+ // Used in tests.
+ _saveRequestAndResponseBodies: false,
+
+ // Chevron width at the starting of Web Console's input box.
+ _chevronWidth: 0,
+ // Width of the monospace characters in Web Console's input box.
+ _inputCharWidth: 0,
+
+ /**
+ * Tells whether to save the bodies of network requests and responses.
+ * Disabled by default to save memory.
+ *
+ * @return boolean
+ * The saveRequestAndResponseBodies pref value.
+ */
+ getSaveRequestAndResponseBodies:
+ function WCF_getSaveRequestAndResponseBodies() {
+ let deferred = promise.defer();
+ let toGet = [
+ "NetworkMonitor.saveRequestAndResponseBodies"
+ ];
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.getPreferences(toGet, aResponse => {
+ if (!aResponse.error) {
+ this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]];
+ deferred.resolve(this._saveRequestAndResponseBodies);
+ }
+ else {
+ deferred.reject(aResponse.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Setter for saving of network request and response bodies.
+ *
+ * @param boolean aValue
+ * The new value you want to set.
+ */
+ setSaveRequestAndResponseBodies:
+ function WCF_setSaveRequestAndResponseBodies(aValue) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let newValue = !!aValue;
+ let toSet = {
+ "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, aResponse => {
+ if (!aResponse.error) {
+ this._saveRequestAndResponseBodies = newValue;
+ deferred.resolve(aResponse);
+ }
+ else {
+ deferred.reject(aResponse.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Getter for the persistent logging preference.
+ * @type boolean
+ */
+ get persistLog() {
+ return Services.prefs.getBoolPref(PREF_PERSISTLOG);
+ },
+
+ /**
+ * Initialize the WebConsoleFrame instance.
+ * @return object
+ * A promise object for the initialization.
+ */
+ init: function WCF_init()
+ {
+ this._initUI();
+ return this._initConnection();
+ },
+
+ /**
+ * 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 WCF__initConnection()
+ {
+ 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);
+ }, (aReason) => { // on failure
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
+ aReason.error + ": " + aReason.message);
+ this.outputMessage(CATEGORY_JS, node, [aReason]);
+ this._initDefer.reject(aReason);
+ }).then(() => {
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created", null);
+ });
+
+ return this._initDefer.promise;
+ },
+
+ /**
+ * Find the Web Console UI elements and setup event listeners as needed.
+ * @private
+ */
+ _initUI: function WCF__initUI()
+ {
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+
+ this._initDefaultFilterPrefs();
+
+ // 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);
+
+ let doc = this.document;
+
+ this.filterBox = doc.querySelector(".hud-filter-box");
+ this.outputNode = doc.getElementById("output-container");
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+
+ this._setFilterTextBoxEvents();
+ this._initFilterButtons();
+
+ let fontSize = this.owner._browserConsole ?
+ Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0;
+
+ if (fontSize != 0) {
+ fontSize = Math.max(MIN_FONT_SIZE, fontSize);
+
+ this.outputNode.style.fontSize = fontSize + "px";
+ this.completeNode.style.fontSize = fontSize + "px";
+ this.inputNode.style.fontSize = fontSize + "px";
+ }
+
+ if (this.owner._browserConsole) {
+ for (let id of ["Enlarge", "Reduce", "Reset"]) {
+ this.document.getElementById("cmd_fullZoom" + id)
+ .removeAttribute("disabled");
+ }
+ }
+
+ // Update the character width and height needed for the popup offset
+ // calculations.
+ this._updateCharSize();
+
+ let updateSaveBodiesPrefUI = (aElement) => {
+ this.getSaveRequestAndResponseBodies().then(aValue => {
+ aElement.setAttribute("checked", aValue);
+ this.emit("save-bodies-ui-toggled");
+ });
+ }
+
+ let reverseSaveBodiesPref = ({ target: aElement }) => {
+ this.getSaveRequestAndResponseBodies().then(aValue => {
+ this.setSaveRequestAndResponseBodies(!aValue);
+ aElement.setAttribute("checked", aValue);
+ this.emit("save-bodies-pref-reversed");
+ });
+ }
+
+ let saveBodiesDisabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("netxhr") &&
+ !this.getFilterState("network");
+
+ let saveBodies = doc.getElementById("saveBodies");
+ saveBodies.addEventListener("command", reverseSaveBodiesPref);
+ saveBodies.disabled = saveBodiesDisabled;
+
+ let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
+ saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref);
+ saveBodiesContextMenu.disabled = saveBodiesDisabled;
+
+ saveBodies.parentNode.addEventListener("popupshowing", () => {
+ updateSaveBodiesPrefUI(saveBodies);
+ saveBodies.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("netxhr") &&
+ !this.getFilterState("network");
+ });
+
+ saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => {
+ updateSaveBodiesPrefUI(saveBodiesContextMenu);
+ saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("netxhr") &&
+ !this.getFilterState("network");
+ });
+
+ let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0];
+ clearButton.addEventListener("command", () => {
+ this.owner._onClearButton();
+ this.jsterm.clearOutput(true);
+ });
+
+ this.jsterm = new JSTerm(this);
+ this.jsterm.init();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+ if (toolbox) {
+ toolbox.on("webconsole-selected", this._onPanelSelected);
+ }
+
+ /*
+ * Focus input line whenever the output area is clicked.
+ * Reusing _addMEssageLinkCallback since it correctly filters
+ * drag and select events.
+ */
+ this._addFocusCallback(this.outputNode, (evt) => {
+ if ((evt.target.nodeName.toLowerCase() != "a") &&
+ (evt.target.parentNode.nodeName.toLowerCase() != "a")) {
+ this.jsterm.inputNode.focus();
+ }
+ });
+
+ // Toggle the timestamp on preference change
+ gDevTools.on("pref-changed", this._onToolboxPrefChanged);
+ this._onToolboxPrefChanged("pref-changed", {
+ pref: PREF_MESSAGE_TIMESTAMP,
+ newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
+ });
+
+ // focus input node
+ this.jsterm.inputNode.focus();
+ },
+
+ /**
+ * Sets the focus to JavaScript input field when the web console tab is
+ * selected or when there is a split console present.
+ * @private
+ */
+ _onPanelSelected: function WCF__onPanelSelected(evt, id)
+ {
+ this.jsterm.inputNode.focus();
+ },
+
+ /**
+ * Initialize the default filter preferences.
+ * @private
+ */
+ _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
+ {
+ let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
+ "exception", "jswarn", "jslog", "error", "info", "warn", "log",
+ "secerror", "secwarn", "netwarn", "netxhr"];
+ for (let pref of prefs) {
+ this.filterPrefs[pref] = Services.prefs
+ .getBoolPref(this._filterPrefsPrefix + pref);
+ }
+ },
+
+ /**
+ * Attach / detach reflow listeners depending on the checked status
+ * of the `CSS > Log` menuitem.
+ *
+ * @param function [aCallback=null]
+ * Optional function to invoke when the listener has been
+ * added/removed.
+ *
+ */
+ _updateReflowActivityListener:
+ function WCF__updateReflowActivityListener(aCallback)
+ {
+ if (this.webConsoleClient) {
+ let pref = this._filterPrefsPrefix + "csslog";
+ if (Services.prefs.getBoolPref(pref)) {
+ this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
+ } else {
+ this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
+ }
+ }
+ },
+
+ /**
+ * Sets the events for the filter input field.
+ * @private
+ */
+ _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
+ {
+ 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 WCF__initFilterButtons()
+ {
+ let categories = this.document
+ .querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(categories, function(aButton) {
+ aButton.addEventListener("contextmenu", (aEvent) => {
+ aButton.open = true;
+ }, false);
+ aButton.addEventListener("click", this._toggleFilter, false);
+
+ let someChecked = false;
+ let severities = aButton.querySelectorAll("menuitem[prefKey]");
+ Array.forEach(severities, function(aMenuItem) {
+ aMenuItem.addEventListener("command", this._toggleFilter, false);
+
+ let prefKey = aMenuItem.getAttribute("prefKey");
+ let checked = this.filterPrefs[prefKey];
+ aMenuItem.setAttribute("checked", checked);
+ someChecked = someChecked || checked;
+ }, this);
+
+ aButton.setAttribute("checked", someChecked);
+ aButton.setAttribute("aria-pressed", someChecked);
+ }, this);
+
+ if (!this.owner._browserConsole) {
+ // 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");
+ }
+ },
+
+ /**
+ * Increase, decrease or reset the font size.
+ *
+ * @param string size
+ * The size of the font change. Accepted values are "+" and "-".
+ * An unmatched size assumes a font reset.
+ */
+ changeFontSize: function WCF_changeFontSize(aSize)
+ {
+ let fontSize = this.window
+ .getComputedStyle(this.outputNode, null)
+ .getPropertyValue("font-size").replace("px", "");
+
+ if (this.outputNode.style.fontSize) {
+ fontSize = this.outputNode.style.fontSize.replace("px", "");
+ }
+
+ if (aSize == "+" || aSize == "-") {
+ fontSize = parseInt(fontSize, 10);
+
+ if (aSize == "+") {
+ fontSize += 1;
+ }
+ else {
+ fontSize -= 1;
+ }
+
+ if (fontSize < MIN_FONT_SIZE) {
+ fontSize = MIN_FONT_SIZE;
+ }
+
+ Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize);
+ fontSize = fontSize + "px";
+
+ this.completeNode.style.fontSize = fontSize;
+ this.inputNode.style.fontSize = fontSize;
+ this.outputNode.style.fontSize = fontSize;
+ }
+ else {
+ this.completeNode.style.fontSize = "";
+ this.inputNode.style.fontSize = "";
+ this.outputNode.style.fontSize = "";
+ Services.prefs.clearUserPref("devtools.webconsole.fontSize");
+ }
+ this._updateCharSize();
+ },
+
+ /**
+ * Calculates the width and height of a single character of the input box.
+ * This will be used in opening the popup at the correct offset.
+ *
+ * @private
+ */
+ _updateCharSize: function WCF__updateCharSize()
+ {
+ let doc = this.document;
+ let tempLabel = doc.createElementNS(XHTML_NS, "span");
+ let style = tempLabel.style;
+ style.position = "fixed";
+ style.padding = "0";
+ style.margin = "0";
+ style.width = "auto";
+ style.color = "transparent";
+ WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
+ tempLabel.textContent = "x";
+ doc.documentElement.appendChild(tempLabel);
+ this._inputCharWidth = tempLabel.offsetWidth;
+ tempLabel.parentNode.removeChild(tempLabel);
+ // Calculate the width of the chevron placed at the beginning of the input
+ // box. Remove 4 more pixels to accomodate the padding of the popup.
+ this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
+ .paddingLeft.replace(/[^0-9.]/g, "") - 4;
+ },
+
+ /**
+ * The event handler that is called whenever a user switches a filter on or
+ * off.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ * The event that triggered the filter change.
+ */
+ _toggleFilter: function WCF__toggleFilter(aEvent)
+ {
+ let target = aEvent.target;
+ let tagName = target.tagName;
+ // Prevent toggle if generated from a contextmenu event (right click)
+ let isRightClick = (aEvent.button === 2); // right click is button 2;
+ if (tagName != aEvent.currentTarget.tagName || isRightClick) {
+ return;
+ }
+
+ switch (tagName) {
+ case "toolbarbutton": {
+ let originalTarget = aEvent.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 (aEvent.getModifierState("Alt")) {
+ let buttons = this.document
+ .querySelectorAll(".webconsole-filter-button");
+ Array.forEach(buttons, (button) => {
+ if (button !== target) {
+ button.setAttribute("checked", false);
+ button.setAttribute("aria-pressed", false);
+ this._setMenuState(button, false);
+ }
+ });
+ state = true;
+ }
+ target.setAttribute("checked", state);
+ target.setAttribute("aria-pressed", state);
+
+ // This is a filter button with a drop-down, and the user clicked the
+ // main part of the button. Go through all the severities and toggle
+ // their associated filters.
+ 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);
+
+ // Disable the log response and request body if network logging is off.
+ if (prefKey == "networkinfo" || prefKey == "netxhr" || prefKey == "network") {
+ let checkState = !this.getFilterState("networkinfo") &&
+ !this.getFilterState("netxhr") &&
+ !this.getFilterState("network");
+ this.document.getElementById("saveBodies").disabled = checkState;
+ this.document.getElementById("saveBodiesContextMenu").disabled = checkState;
+ }
+
+ // 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 aTarget
+ * Button with drop down items to be toggled.
+ * @param boolean aState
+ * True if the menu item is being toggled on, and false otherwise.
+ */
+ _setMenuState: function WCF__setMenuState(aTarget, aState)
+ {
+ let menuItems = aTarget.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ item.setAttribute("checked", aState);
+ let prefKey = item.getAttribute("prefKey");
+ this.setFilterState(prefKey, aState);
+ });
+ },
+
+ /**
+ * Set the filter state for a specific toggle button.
+ *
+ * @param string aToggleType
+ * @param boolean aState
+ * @returns void
+ */
+ setFilterState: function WCF_setFilterState(aToggleType, aState)
+ {
+ this.filterPrefs[aToggleType] = aState;
+ this.adjustVisibilityForMessageType(aToggleType, aState);
+ Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
+ this._updateReflowActivityListener();
+ },
+
+ /**
+ * Get the filter state for a specific toggle button.
+ *
+ * @param string aToggleType
+ * @returns boolean
+ */
+ getFilterState: function WCF_getFilterState(aToggleType)
+ {
+ return this.filterPrefs[aToggleType];
+ },
+
+ /**
+ * Check that the passed string matches the filter arguments.
+ *
+ * @param String aString
+ * to search for filter words in.
+ * @param String aFilter
+ * is a string containing all of the words to filter on.
+ * @returns boolean
+ */
+ stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter)
+ {
+ if (!aFilter || !aString) {
+ return true;
+ }
+
+ let searchStr = aString.toLowerCase();
+ let filterStrings = aFilter.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 @aPrefKey.
+ *
+ * @param string aPrefKey
+ * The preference key for the message type being filtered: one of the
+ * values in the MESSAGE_PREFERENCE_KEYS table.
+ * @param boolean aState
+ * True if the filter named by @aMessageType is being turned on; false
+ * otherwise.
+ * @returns void
+ */
+ adjustVisibilityForMessageType:
+ function WCF_adjustVisibilityForMessageType(aPrefKey, aState)
+ {
+ 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 xpath = ".//*[contains(@class, 'message') and " +
+ "@filter='" + aPrefKey + "']";
+ let result = doc.evaluate(xpath, outputNode, null,
+ Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (let i = 0; i < result.snapshotLength; i++) {
+ let node = result.snapshotItem(i);
+ if (aState) {
+ node.classList.remove("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 WCF_adjustVisibilityOnSearchStringChange()
+ {
+ 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");
+ }
+ }
+ },
+
+ /**
+ * Applies the user's filters to a newly-created message node via CSS
+ * classes.
+ *
+ * @param nsIDOMNode aNode
+ * The newly-created message node.
+ * @return boolean
+ * True if the message was filtered or false otherwise.
+ */
+ filterMessageNode: function WCF_filterMessageNode(aNode)
+ {
+ let isFiltered = false;
+
+ // Filter by the message type.
+ let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
+ if (prefKey && !this.getFilterState(prefKey)) {
+ // The node is filtered by type.
+ aNode.classList.add("filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter on the search string.
+ let search = this.filterBox.value;
+ let text = aNode.clipboardText;
+
+ // if string matches the filter text
+ if (!this.stringMatchesFilters(text, search)) {
+ aNode.classList.add("filtered-by-string");
+ isFiltered = true;
+ }
+
+ if (isFiltered && aNode.classList.contains("inlined-variables-view")) {
+ aNode.classList.add("hidden-message");
+ }
+
+ return isFiltered;
+ },
+
+ /**
+ * Merge the attributes of the two nodes that are about to be filtered.
+ * Increment the number of repeats of aOriginal.
+ *
+ * @param nsIDOMNode aOriginal
+ * The Original Node. The one being merged into.
+ * @param nsIDOMNode aFiltered
+ * The node being filtered out because it is repeated.
+ */
+ mergeFilteredMessageNode:
+ function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
+ {
+ let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ return; // no repeat node, return early.
+ }
+
+ let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
+ repeatNode.setAttribute("value", occurrences);
+ repeatNode.textContent = occurrences;
+ let str = l10n.getStr("messageRepeats.tooltip2");
+ repeatNode.title = PluralForm.get(occurrences, str)
+ .replace("#1", occurrences);
+ },
+
+ /**
+ * Filter the message node from the output if it is a repeat.
+ *
+ * @private
+ * @param nsIDOMNode aNode
+ * The message node to be filtered or not.
+ * @returns nsIDOMNode|null
+ * Returns the duplicate node if the message was filtered, null
+ * otherwise.
+ */
+ _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode)
+ {
+ let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ return null;
+ }
+
+ let uid = repeatNode._uid;
+ let dupeNode = null;
+
+ if (aNode.category == CATEGORY_CSS ||
+ aNode.category == CATEGORY_SECURITY) {
+ dupeNode = this._repeatNodes[uid];
+ if (!dupeNode) {
+ this._repeatNodes[uid] = aNode;
+ }
+ }
+ else if ((aNode.category == CATEGORY_WEBDEV ||
+ aNode.category == CATEGORY_JS) &&
+ aNode.category != CATEGORY_NETWORK &&
+ !aNode.classList.contains("inlined-variables-view")) {
+ let lastMessage = this.outputNode.lastChild;
+ if (!lastMessage) {
+ return null;
+ }
+
+ let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0];
+ if (lastRepeatNode && lastRepeatNode._uid == uid) {
+ dupeNode = lastMessage;
+ }
+ }
+
+ if (dupeNode) {
+ this.mergeFilteredMessageNode(dupeNode, aNode);
+ return dupeNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Display cached messages that may have been collected before the UI is
+ * displayed.
+ *
+ * @param array aRemoteMessages
+ * Array of cached messages coming from the remote Web Console
+ * content instance.
+ */
+ displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages)
+ {
+ if (!aRemoteMessages.length) {
+ return;
+ }
+
+ aRemoteMessages.forEach(function(aMessage) {
+ switch (aMessage._type) {
+ case "PageError": {
+ let category = Utils.categoryForScriptError(aMessage);
+ this.outputMessage(category, this.reportPageError,
+ [category, aMessage]);
+ break;
+ }
+ case "LogMessage":
+ this.handleLogMessage(aMessage);
+ break;
+ case "ConsoleAPI":
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
+ [aMessage]);
+ break;
+ }
+ }, this);
+ },
+
+ /**
+ * Logs a message to the Web Console that originates from the Web Console
+ * server.
+ *
+ * @param object aMessage
+ * The message received from the server.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage)
+ {
+ let body = null;
+ let clipboardText = null;
+ let sourceURL = aMessage.filename;
+ let sourceLine = aMessage.lineNumber;
+ let level = aMessage.level;
+ let args = aMessage.arguments;
+ let objectActors = new Set();
+ let node = null;
+
+ // Gather the actor IDs.
+ args.forEach((aValue) => {
+ if (WebConsoleUtils.isActorGrip(aValue)) {
+ objectActors.add(aValue.actor);
+ }
+ });
+
+ switch (level) {
+ case "log":
+ case "info":
+ case "warn":
+ case "error":
+ case "exception":
+ case "assert":
+ case "debug": {
+ let msg = new Messages.ConsoleGeneric(aMessage);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "table": {
+ let msg = new Messages.ConsoleTable(aMessage);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "trace": {
+ let msg = new Messages.ConsoleTrace(aMessage);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "dir": {
+ body = { arguments: args };
+ let clipboardArray = [];
+ args.forEach((aValue) => {
+ clipboardArray.push(VariablesView.getString(aValue));
+ });
+ clipboardText = clipboardArray.join(" ");
+ break;
+ }
+
+ case "group":
+ case "groupCollapsed":
+ clipboardText = body = aMessage.groupName;
+ this.groupDepth++;
+ break;
+
+ case "groupEnd":
+ if (this.groupDepth > 0) {
+ this.groupDepth--;
+ }
+ break;
+
+ case "time": {
+ let timer = aMessage.timer;
+ if (!timer) {
+ return null;
+ }
+ if (timer.error) {
+ Cu.reportError(l10n.getStr(timer.error));
+ return null;
+ }
+ body = l10n.getFormatStr("timerStarted", [timer.name]);
+ clipboardText = body;
+ break;
+ }
+
+ case "timeEnd": {
+ let timer = aMessage.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 = aMessage.counter;
+ if (!counter) {
+ return null;
+ }
+ if (counter.error) {
+ Cu.reportError(l10n.getStr(counter.error));
+ return null;
+ }
+ let msg = new Messages.ConsoleGeneric(aMessage);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+
+ default:
+ Cu.reportError("Unknown Console API log level: " + level);
+ return null;
+ }
+
+ // 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") {
+ return null; // no need to continue
+ }
+
+ if (!node) {
+ node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
+ sourceURL, sourceLine, clipboardText,
+ level, aMessage.timeStamp);
+ if (aMessage.private) {
+ node.setAttribute("private", true);
+ }
+ }
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+
+ if (!node._messageObject) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ repeatNode._uid += [...objectActors].join("-");
+ }
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle ConsoleAPICall objects received from the server. This method outputs
+ * the window.console API call.
+ *
+ * @param object aMessage
+ * The console API message received from the server.
+ */
+ handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage)
+ {
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
+ },
+
+ /**
+ * Reports an error in the page source, either JavaScript or CSS.
+ *
+ * @param nsIScriptError aScriptError
+ * The error message to report.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ reportPageError: function WCF_reportPageError(aCategory, aScriptError)
+ {
+ // Warnings and legacy strict errors become warnings; other types become
+ // errors.
+ let severity = 'error';
+ if (aScriptError.warning || aScriptError.strict) {
+ severity = 'warning';
+ }
+
+ let category = 'js';
+ switch(aCategory) {
+ case CATEGORY_CSS:
+ category = 'css';
+ break;
+ case CATEGORY_SECURITY:
+ category = 'security';
+ break;
+ }
+
+ let objectActors = new Set();
+
+ // Gather the actor IDs.
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = aScriptError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ objectActors.add(grip.actor);
+ }
+ }
+
+ let errorMessage = aScriptError.errorMessage;
+ if (errorMessage.type && errorMessage.type == "longString") {
+ errorMessage = errorMessage.initial;
+ }
+
+ // Create a new message
+ let msg = new Messages.Simple(errorMessage, {
+ location: {
+ url: aScriptError.sourceName,
+ line: aScriptError.lineNumber,
+ column: aScriptError.columnNumber
+ },
+ category: category,
+ severity: severity,
+ timestamp: aScriptError.timeStamp,
+ private: aScriptError.private,
+ filterDuplicates: true
+ });
+
+ let node = msg.init(this.output).render().element;
+
+ // Select the body of the message node that is displayed in the console
+ let msgBody = node.getElementsByClassName("message-body")[0];
+ // Add the more info link node to messages that belong to certain categories
+ this.addMoreInfoLink(msgBody, aScriptError);
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle PageError objects received from the server. This method outputs the
+ * given error.
+ *
+ * @param nsIScriptError aPageError
+ * The error received from the server.
+ */
+ handlePageError: function WCF_handlePageError(aPageError)
+ {
+ let category = Utils.categoryForScriptError(aPageError);
+ this.outputMessage(category, this.reportPageError, [category, aPageError]);
+ },
+
+ /**
+ * Handle log messages received from the server. This method outputs the given
+ * message.
+ *
+ * @param object aPacket
+ * The message packet received from the server.
+ */
+ handleLogMessage: function WCF_handleLogMessage(aPacket)
+ {
+ if (aPacket.message) {
+ this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]);
+ }
+ },
+
+ /**
+ * Display log messages received from the server.
+ *
+ * @private
+ * @param object aPacket
+ * The message packet received from the server.
+ * @return nsIDOMElement
+ * The message element to render for the given log message.
+ */
+ _reportLogMessage: function WCF__reportLogMessage(aPacket)
+ {
+ let msg = aPacket.message;
+ if (msg.type && msg.type == "longString") {
+ msg = msg.initial;
+ }
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
+ null, null, null, aPacket.timeStamp);
+ if (WebConsoleUtils.isActorGrip(aPacket.message)) {
+ node._objectActors = new Set([aPacket.message.actor]);
+ }
+ return node;
+ },
+
+ /**
+ * Log network event.
+ *
+ * @param object aActor
+ * The network event actor to log.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logNetEvent: function WCF_logNetEvent(aActor)
+ {
+ let actorId = aActor.actor;
+ let networkInfo = this._networkRequests[actorId];
+ if (!networkInfo) {
+ return null;
+ }
+
+ 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);
+ 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 = () => {
+ if (!messageNode._panelOpen) {
+ this.openNetworkPanel(messageNode, networkInfo);
+ }
+ };
+
+ this._addMessageLinkCallback(urlNode, onClick);
+ this._addMessageLinkCallback(statusNode, onClick);
+
+ networkInfo.node = messageNode;
+
+ this._updateNetMessage(actorId);
+
+ return messageNode;
+ },
+
+ /**
+ * Create a mixed content warning Node.
+ *
+ * @param aLinkNode
+ * Parent to the requested urlNode.
+ */
+ makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
+ {
+ 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;
+
+ aLinkNode.appendChild(mixedContentWarningNode);
+
+ this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => {
+ aEvent.stopPropagation();
+ this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
+ });
+ },
+
+ /**
+ * Adds a more info link node to messages based on the nsIScriptError object
+ * that we need to report to the console
+ *
+ * @param aNode
+ * The node to which we will be adding the more info link node
+ * @param aScriptError
+ * The script error object that we are reporting to the console
+ */
+ addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError)
+ {
+ let url;
+ switch (aScriptError.category) {
+ case "Insecure Password Field":
+ url = INSECURE_PASSWORDS_LEARN_MORE;
+ break;
+ case "Mixed Content Message":
+ case "Mixed Content Blocker":
+ url = MIXED_CONTENT_LEARN_MORE;
+ break;
+ case "Invalid HSTS Headers":
+ url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
+ break;
+ case "SHA-1 Signature":
+ url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE;
+ break;
+ default:
+ // Unknown category. Return without adding more info node.
+ return;
+ }
+
+ this.addLearnMoreWarningNode(aNode, url);
+ },
+
+ /*
+ * Appends a clickable warning node to the node passed
+ * as a parameter to the function. When a user clicks on the appended
+ * warning node, the browser navigates to the provided url.
+ *
+ * @param aNode
+ * The node to which we will be adding a clickable warning node.
+ * @param aURL
+ * The url which points to the page where the user can learn more
+ * about security issues associated with the specific message that's
+ * being logged.
+ */
+ addLearnMoreWarningNode:
+ function WCF_addLearnMoreWarningNode(aNode, aURL)
+ {
+ let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
+
+ let warningNode = this.document.createElementNS(XHTML_NS, "a");
+ warningNode.title = aURL;
+ warningNode.href = aURL;
+ warningNode.draggable = false;
+ warningNode.textContent = moreInfoLabel;
+ warningNode.className = "learn-more-link";
+
+ this._addMessageLinkCallback(warningNode, (aEvent) => {
+ aEvent.stopPropagation();
+ this.owner.openLink(aURL);
+ });
+
+ aNode.appendChild(warningNode);
+ },
+
+ /**
+ * Log file activity.
+ *
+ * @param string aFileURI
+ * The file URI that was loaded.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ logFileActivity: function WCF_logFileActivity(aFileURI)
+ {
+ let urlNode = this.document.createElementNS(XHTML_NS, "a");
+ urlNode.setAttribute("title", aFileURI);
+ urlNode.className = "url";
+ urlNode.textContent = aFileURI;
+ urlNode.draggable = false;
+ urlNode.href = aFileURI;
+
+ let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
+ urlNode, null, null, aFileURI);
+
+ this._addMessageLinkCallback(urlNode, () => {
+ this.owner.viewSource(aFileURI);
+ });
+
+ return outputNode;
+ },
+
+ /**
+ * Handle the file activity messages coming from the remote Web Console.
+ *
+ * @param string aFileURI
+ * The file URI that was requested.
+ */
+ handleFileActivity: function WCF_handleFileActivity(aFileURI)
+ {
+ this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
+ },
+
+ /**
+ * Handle the reflow activity messages coming from the remote Web Console.
+ *
+ * @param object aMessage
+ * An object holding information about a reflow batch.
+ */
+ logReflowActivity: function WCF_logReflowActivity(aMessage)
+ {
+ let {start, end, sourceURL, sourceLine} = aMessage;
+ let duration = Math.round((end - start) * 100) / 100;
+ let node = this.document.createElementNS(XHTML_NS, "span");
+ if (sourceURL) {
+ node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]);
+ let a = this.document.createElementNS(XHTML_NS, "a");
+ a.href = "#";
+ a.draggable = "false";
+ let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL);
+ let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction");
+ a.textContent = l10n.getFormatStr("reflow.messageLinkText",
+ [functionName, filename, sourceLine]);
+ this._addMessageLinkCallback(a, () => {
+ this.owner.viewSourceInDebugger(sourceURL, sourceLine);
+ });
+ node.appendChild(a);
+ } else {
+ node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
+ }
+ return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
+ },
+
+
+ handleReflowActivity: function WCF_handleReflowActivity(aMessage)
+ {
+ this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]);
+ },
+
+ /**
+ * Inform user that the window.console API has been replaced by a script
+ * in a content page.
+ */
+ logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
+ {
+ 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 aActor
+ * The NetworkEventActor grip.
+ */
+ handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
+ {
+ let networkInfo = {
+ node: null,
+ actor: aActor.actor,
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: aActor.startedDateTime,
+ request: {
+ url: aActor.url,
+ method: aActor.method,
+ },
+ isXHR: aActor.isXHR,
+ response: {},
+ timings: {},
+ updates: [], // track the list of network event updates
+ private: aActor.private,
+ };
+
+ this._networkRequests[aActor.actor] = networkInfo;
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor]);
+ },
+
+ /**
+ * Handle network event updates coming from the server.
+ *
+ * @param string aActorId
+ * The network event actor ID.
+ * @param string aType
+ * Update type.
+ * @param object aPacket
+ * Update details.
+ */
+ handleNetworkEventUpdate:
+ function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
+ {
+ let networkInfo = this._networkRequests[aActorId];
+ if (!networkInfo) {
+ return;
+ }
+
+ networkInfo.updates.push(aType);
+
+ switch (aType) {
+ case "requestHeaders":
+ networkInfo.request.headersSize = aPacket.headersSize;
+ break;
+ case "requestPostData":
+ networkInfo.discardRequestBody = aPacket.discardRequestBody;
+ networkInfo.request.bodySize = aPacket.dataSize;
+ break;
+ case "responseStart":
+ networkInfo.response.httpVersion = aPacket.response.httpVersion;
+ networkInfo.response.status = aPacket.response.status;
+ networkInfo.response.statusText = aPacket.response.statusText;
+ networkInfo.response.headersSize = aPacket.response.headersSize;
+ networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
+ break;
+ case "responseContent":
+ networkInfo.response.content = {
+ mimeType: aPacket.mimeType,
+ };
+ networkInfo.response.bodySize = aPacket.contentSize;
+ networkInfo.discardResponseBody = aPacket.discardResponseBody;
+ break;
+ case "eventTimings":
+ networkInfo.totalTime = aPacket.totalTime;
+ break;
+ }
+
+ if (networkInfo.node && this._updateNetMessage(aActorId)) {
+ this.emit("new-messages", new Set([{
+ update: true,
+ node: networkInfo.node,
+ response: aPacket,
+ }]));
+ }
+
+ // For unit tests we pass the HTTP activity object to the test callback,
+ // 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 aActorId
+ * The network event actor ID for which you want to update the message.
+ * @return boolean
+ * |true| if the message node was updated, or |false| otherwise.
+ */
+ _updateNetMessage: function WCF__updateNetMessage(aActorId)
+ {
+ let networkInfo = this._networkRequests[aActorId];
+ if (!networkInfo || !networkInfo.node) {
+ return;
+ }
+
+ 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 a NetworkPanel.
+ *
+ * @param nsIDOMNode aNode
+ * The message node you want the panel to be anchored to.
+ * @param object aHttpActivity
+ * The HTTP activity object that holds network request and response
+ * information. This object is given to the NetworkPanel constructor.
+ * @return object
+ * The new NetworkPanel instance.
+ */
+ openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity)
+ {
+ let actor = aHttpActivity.actor;
+
+ if (actor) {
+ this.webConsoleClient.getRequestHeaders(actor, (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.headers = aResponse.headers;
+
+ this.webConsoleClient.getRequestCookies(actor, onRequestCookies);
+ });
+ }
+
+ let onRequestCookies = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestCookies:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.cookies = aResponse.cookies;
+
+ this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders);
+ };
+
+ let onResponseHeaders = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.headers = aResponse.headers;
+
+ this.webConsoleClient.getResponseCookies(actor, onResponseCookies);
+ };
+
+ let onResponseCookies = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseCookies:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.cookies = aResponse.cookies;
+
+ this.webConsoleClient.getRequestPostData(actor, onRequestPostData);
+ };
+
+ let onRequestPostData = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getRequestPostData:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.request.postData = aResponse.postData;
+ aHttpActivity.discardRequestBody = aResponse.postDataDiscarded;
+
+ this.webConsoleClient.getResponseContent(actor, onResponseContent);
+ };
+
+ let onResponseContent = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getResponseContent:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.response.content = aResponse.content;
+ aHttpActivity.discardResponseBody = aResponse.contentDiscarded;
+
+ this.webConsoleClient.getEventTimings(actor, onEventTimings);
+ };
+
+ let onEventTimings = (aResponse) => {
+ if (aResponse.error) {
+ Cu.reportError("WCF_openNetworkPanel getEventTimings:" +
+ aResponse.error);
+ return;
+ }
+
+ aHttpActivity.timings = aResponse.timings;
+
+ openPanel();
+ };
+
+ let openPanel = () => {
+ aNode._netPanel = netPanel;
+
+ let panel = netPanel.panel;
+ panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
+ panel.sizeTo(450, 500);
+ panel.setAttribute("hudId", this.hudId);
+
+ panel.addEventListener("popuphiding", function WCF_netPanel_onHide() {
+ panel.removeEventListener("popuphiding", WCF_netPanel_onHide);
+
+ aNode._panelOpen = false;
+ aNode._netPanel = null;
+ });
+
+ aNode._panelOpen = true;
+ };
+
+ let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this);
+ netPanel.linkNode = aNode;
+
+ if (!actor) {
+ openPanel();
+ }
+
+ return netPanel;
+ },
+
+ /**
+ * Handler for page location changes.
+ *
+ * @param string aURI
+ * New page location.
+ * @param string aTitle
+ * New page title.
+ */
+ onLocationChange: function WCF_onLocationChange(aURI, aTitle)
+ {
+ this.contentLocation = aURI;
+ if (this.owner.onLocationChange) {
+ this.owner.onLocationChange(aURI, aTitle);
+ }
+ },
+
+ /**
+ * Handler for the tabNavigated notification.
+ *
+ * @param string aEvent
+ * Event name.
+ * @param object aPacket
+ * Notification packet received from the server.
+ */
+ handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket)
+ {
+ if (aEvent == "will-navigate") {
+ if (this.persistLog) {
+ let marker = new Messages.NavigationMarker(aPacket, Date.now());
+ this.output.addMessage(marker);
+ }
+ else {
+ this.jsterm.clearOutput();
+ }
+ }
+
+ if (aPacket.url) {
+ this.onLocationChange(aPacket.url, aPacket.title);
+ }
+
+ if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) {
+ this.logWarningAboutReplacedAPI();
+ }
+ },
+
+ /**
+ * Output a message node. This filters a node appropriately, then sends it to
+ * the output, regrouping and pruning output as necessary.
+ *
+ * Note: this call is async - the given message node may not be displayed when
+ * you call this method.
+ *
+ * @param integer aCategory
+ * The category of the message you want to output. See the CATEGORY_*
+ * constants.
+ * @param function|nsIDOMElement aMethodOrNode
+ * 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 |aArguments|.
+ * @param array [aArguments]
+ * If a method is given to output the message element then the method
+ * will be invoked with the list of arguments given here. The last
+ * object in this array should be the packet received from the
+ * back end.
+ */
+ outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments)
+ {
+ 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([aCategory, aMethodOrNode, aArguments]);
+
+ 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 WCF__flushMessageQueue()
+ {
+ 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 = outputNode.parentNode;
+ let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
+
+ // We won't bother to try to restore scroll position if this is showing
+ // a lot of messages at once (and there are still items in the queue).
+ // It is going to purge whatever you were looking at anyway.
+ let scrolledToBottom = shouldPrune ||
+ Utils.isOutputScrolledToBottom(outputNode);
+
+ // Output the current batch of messages.
+ let 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(aCategory) {
+ removedNodes += this.pruneOutputIfNecessary(aCategory);
+ }, 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();
+
+ this._lastOutputFlush = Date.now();
+ },
+
+ /**
+ * Initialize the output timer.
+ * @private
+ */
+ _initOutputTimer: function WCF__initOutputTimer()
+ {
+ 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 aHudIdSupportsString
+ * The HUD ID as an nsISupportsString.
+ * @param array aItem
+ * 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 WCF__outputMessageFromQueue(aHudIdSupportsString, aItem)
+ {
+ let [category, methodOrNode, args] = aItem;
+
+ // The last object in the args array should be message
+ // object or response packet received from the server.
+ let message = (args && args.length) ? args[args.length-1] : null;
+
+ let node = typeof methodOrNode == "function" ?
+ methodOrNode.apply(this, args || []) :
+ methodOrNode;
+ if (!node) {
+ return null;
+ }
+
+ let afterNode = node._outputAfterNode;
+ if (afterNode) {
+ delete node._outputAfterNode;
+ }
+
+ let isFiltered = this.filterMessageNode(node);
+
+ let isRepeated = this._filterRepeatedMessage(node);
+
+ let visible = !isRepeated && !isFiltered;
+ if (!isRepeated) {
+ this.outputNode.insertBefore(node,
+ afterNode ? afterNode.nextSibling : null);
+ this._pruneCategoriesQueue[node.category] = true;
+
+ let nodeID = node.getAttribute("id");
+ Services.obs.notifyObservers(aHudIdSupportsString,
+ "web-console-message-created", nodeID);
+
+ }
+
+ 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 WCF__pruneOutputQueue()
+ {
+ let nodes = {};
+
+ // Group the messages per category.
+ this._outputQueue.forEach(function(aItem, aIndex) {
+ let [category] = aItem;
+ if (!(category in nodes)) {
+ nodes[category] = [];
+ }
+ nodes[category].push(aIndex);
+ }, this);
+
+ let pruned = 0;
+
+ // Loop through the categories we found and prune if needed.
+ for (let category in nodes) {
+ let limit = 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 aItem
+ * The item you want to destroy. Does not remove it from the output
+ * queue.
+ */
+ _destroyItem: function WCF__destroyItem(aItem)
+ {
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ let [category, methodOrNode, args] = aItem;
+ if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
+ for (let actor of methodOrNode._objectActors) {
+ 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 && connectionId in this._networkRequests) {
+ delete this._networkRequests[connectionId];
+ this._releaseObject(connectionId);
+ }
+ }
+ else if (category == CATEGORY_WEBDEV &&
+ methodOrNode == this.logConsoleAPIMessage) {
+ args[0].arguments.forEach((aValue) => {
+ if (WebConsoleUtils.isActorGrip(aValue)) {
+ this._releaseObject(aValue.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);
+ }
+ }
+ },
+
+ /**
+ * Ensures that the number of message nodes of type aCategory don't exceed that
+ * category's line limit by removing old messages as needed.
+ *
+ * @param integer aCategory
+ * The category of message nodes to prune if needed.
+ * @return number
+ * The number of removed nodes.
+ */
+ pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory)
+ {
+ let logLimit = Utils.logLimitForCategory(aCategory);
+ let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
+ CATEGORY_CLASS_FRAGMENTS[aCategory] + "]");
+ 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 aNode
+ * The message node you want to remove.
+ */
+ removeOutputMessage: function WCF_removeOutputMessage(aNode)
+ {
+ if (aNode._messageObject) {
+ aNode._messageObject.destroy();
+ }
+
+ if (aNode._objectActors) {
+ for (let actor of aNode._objectActors) {
+ this._releaseObject(actor);
+ }
+ aNode._objectActors.clear();
+ }
+
+ if (aNode.category == CATEGORY_CSS ||
+ aNode.category == CATEGORY_SECURITY) {
+ let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
+ if (repeatNode && repeatNode._uid) {
+ delete this._repeatNodes[repeatNode._uid];
+ }
+ }
+ else if (aNode._connectionId &&
+ aNode.category == CATEGORY_NETWORK) {
+ delete this._networkRequests[aNode._connectionId];
+ this._releaseObject(aNode._connectionId);
+ }
+ else if (aNode.classList.contains("inlined-variables-view")) {
+ let view = aNode._variablesView;
+ if (view) {
+ view.controller.releaseActors();
+ }
+ aNode._variablesView = null;
+ }
+
+ aNode.remove();
+ },
+
+ /**
+ * Given a category and message body, creates a DOM node to represent an
+ * incoming message. The timestamp is automatically added.
+ *
+ * @param number aCategory
+ * The category of the message: one of the CATEGORY_* constants.
+ * @param number aSeverity
+ * The severity of the message: one of the SEVERITY_* constants;
+ * @param string|nsIDOMNode aBody
+ * The body of the message, either a simple string or a DOM node.
+ * @param string aSourceURL [optional]
+ * The URL of the source file that emitted the error.
+ * @param number aSourceLine [optional]
+ * The line number on which the error occurred. If zero or omitted,
+ * there is no line number associated with this message.
+ * @param string aClipboardText [optional]
+ * The text that should be copied to the clipboard when this node is
+ * copied. If omitted, defaults to the body text. If `aBody` is not
+ * a string, then the clipboard text must be supplied.
+ * @param number aLevel [optional]
+ * The level of the console API message.
+ * @param number aTimeStamp [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 WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
+ aSourceLine, aClipboardText, aLevel, aTimeStamp)
+ {
+ if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
+ aClipboardText = aBody.innerText;
+ }
+
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+
+ // Apply the current group by indenting appropriately.
+ let indent = this.groupDepth * GROUP_INDENT;
+ indentNode.style.width = indent + "px";
+
+ // Make the icon container, which is a vertical box. Its purpose is to
+ // ensure that the icon stays anchored at the top of the message even for
+ // long multi-line messages.
+ let iconContainer = this.document.createElementNS(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 body = aBody;
+ // If a string was supplied for the body, turn it into a DOM node and an
+ // associated clipboard string now.
+ aClipboardText = aClipboardText ||
+ (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
+ (aSourceLine ? ":" + aSourceLine : ""));
+
+ let timestamp = aTimeStamp || Date.now();
+
+ // Create the containing node and append all its elements to it.
+ let node = this.document.createElementNS(XHTML_NS, "div");
+ node.id = "console-msg-" + gSequenceId();
+ node.className = "message";
+ node.clipboardText = aClipboardText;
+ node.timestamp = timestamp;
+ this.setMessageType(node, aCategory, aSeverity);
+
+ if (aBody instanceof Ci.nsIDOMNode) {
+ bodyNode.appendChild(aBody);
+ }
+ else {
+ let str = undefined;
+ if (aLevel == "dir") {
+ str = VariablesView.getString(aBody.arguments[0]);
+ }
+ else {
+ str = aBody;
+ }
+
+ if (str !== undefined) {
+ aBody = this.document.createTextNode(str);
+ bodyNode.appendChild(aBody);
+ }
+ }
+
+ // Add the message repeats node only when needed.
+ let repeatNode = null;
+ if (aCategory != CATEGORY_INPUT &&
+ aCategory != CATEGORY_OUTPUT &&
+ aCategory != CATEGORY_NETWORK &&
+ !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
+ repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
+ aSourceURL, aSourceLine].join(":");
+ }
+
+ // Create the timestamp.
+ let timestampNode = this.document.createElementNS(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 (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
+ locationNode = this.createLocationNode({url: aSourceURL,
+ line: aSourceLine});
+ }
+
+ node.appendChild(timestampNode);
+ node.appendChild(indentNode);
+ node.appendChild(iconContainer);
+
+ // Display the variables view after the message node.
+ if (aLevel == "dir") {
+ bodyNode.style.height = (this.window.innerHeight *
+ CONSOLE_DIR_VIEW_HEIGHT) + "px";
+
+ let options = {
+ objectActor: body.arguments[0],
+ targetElement: bodyNode,
+ hideFilterInput: true,
+ };
+ this.jsterm.openVariablesView(options).then((aView) => {
+ node._variablesView = aView;
+ 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 aLocation
+ * An object containing url, line and column number of the message source (destructured).
+ * @param string aTarget [optional]
+ * Tells which tool to open the link with, on click. Supported tools:
+ * jsdebugger, styleeditor, scratchpad.
+ * @return nsIDOMNode
+ * The new anchor element, ready to be added to the message node.
+ */
+ createLocationNode:
+ function WCF_createLocationNode({url, line, column}, aTarget)
+ {
+ if (!url) {
+ url = "";
+ }
+ let locationNode = this.document.createElementNS(XHTML_NS, "a");
+ let filenameNode = this.document.createElementNS(XHTML_NS, "span");
+
+ // Create the text, which consists of an abbreviated version of the URL
+ // Scratchpad URLs should not be abbreviated.
+ let filename;
+ let fullURL;
+ let isScratchpad = false;
+
+ if (/^Scratchpad\/\d+$/.test(url)) {
+ filename = url;
+ fullURL = url;
+ isScratchpad = true;
+ }
+ else {
+ fullURL = url.split(" -> ").pop();
+ filename = WebConsoleUtils.abbreviateSourceURL(fullURL);
+ }
+
+ filenameNode.className = "filename";
+ filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation"));
+ locationNode.appendChild(filenameNode);
+
+ locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
+ locationNode.draggable = false;
+ if (aTarget) {
+ locationNode.target = aTarget;
+ }
+ locationNode.setAttribute("title", url);
+ locationNode.className = "message-location theme-link devtools-monospace";
+
+ // Make the location clickable.
+ let onClick = () => {
+ let target = locationNode.target;
+ if (target == "scratchpad" || isScratchpad) {
+ this.owner.viewSourceInScratchpad(url);
+ return;
+ }
+
+ let category = locationNode.parentNode.category;
+ if (target == "styleeditor" || category == CATEGORY_CSS) {
+ this.owner.viewSourceInStyleEditor(fullURL, line);
+ }
+ else if (target == "jsdebugger" ||
+ category == CATEGORY_JS || category == CATEGORY_WEBDEV) {
+ this.owner.viewSourceInDebugger(fullURL, line);
+ }
+ else {
+ this.owner.viewSource(fullURL, line);
+ }
+ };
+
+ if (fullURL) {
+ this._addMessageLinkCallback(locationNode, onClick);
+ }
+
+ if (line) {
+ let lineNumberNode = this.document.createElementNS(XHTML_NS, "span");
+ lineNumberNode.className = "line-number";
+ lineNumberNode.textContent = ":" + line + (column >= 0 ? ":" + column : "");
+ locationNode.appendChild(lineNumberNode);
+ locationNode.sourceLine = line;
+ }
+
+ return locationNode;
+ },
+
+ /**
+ * Adjusts the category and severity of the given message.
+ *
+ * @param nsIDOMNode aMessageNode
+ * The message node to alter.
+ * @param number aCategory
+ * The category for the message; one of the CATEGORY_ constants.
+ * @param number aSeverity
+ * The severity for the message; one of the SEVERITY_ constants.
+ * @return void
+ */
+ setMessageType:
+ function WCF_setMessageType(aMessageNode, aCategory, aSeverity)
+ {
+ aMessageNode.category = aCategory;
+ aMessageNode.severity = aSeverity;
+ aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]);
+ aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]);
+ aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]);
+ },
+
+ /**
+ * Add the mouse event handlers needed to make a link.
+ *
+ * @private
+ * @param nsIDOMNode aNode
+ * The node for which you want to add the event handlers.
+ * @param function aCallback
+ * The function you want to invoke on click.
+ */
+ _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
+ {
+ aNode.addEventListener("mousedown", (aEvent) => {
+ this._mousedown = true;
+ this._startX = aEvent.clientX;
+ this._startY = aEvent.clientY;
+ }, false);
+
+ aNode.addEventListener("click", (aEvent) => {
+ let mousedown = this._mousedown;
+ this._mousedown = false;
+
+ aEvent.preventDefault();
+
+ // Do not allow middle/right-click or 2+ clicks.
+ if (aEvent.detail != 1 || aEvent.button != 0) {
+ return;
+ }
+
+ // If this event started with a mousedown event and it ends at a different
+ // location, we consider this text selection.
+ if (mousedown &&
+ (this._startX != aEvent.clientX) &&
+ (this._startY != aEvent.clientY))
+ {
+ this._startX = this._startY = undefined;
+ return;
+ }
+
+ this._startX = this._startY = undefined;
+
+ aCallback.call(this, aEvent);
+ }, false);
+ },
+
+ _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback)
+ {
+ aNode.addEventListener("mousedown", (aEvent) => {
+ this._mousedown = true;
+ this._startX = aEvent.clientX;
+ this._startY = aEvent.clientY;
+ }, false);
+
+ aNode.addEventListener("click", (aEvent) => {
+ let mousedown = this._mousedown;
+ this._mousedown = false;
+
+ // Do not allow middle/right-click or 2+ clicks.
+ if (aEvent.detail != 1 || aEvent.button != 0) {
+ return;
+ }
+
+ // If this event started with a mousedown event and it ends at a different
+ // location, we consider this text selection.
+ // Add a fuzz modifier of two pixels in any direction to account for sloppy
+ // clicking.
+ if (mousedown &&
+ (Math.abs(aEvent.clientX - this._startX) >= 2) &&
+ (Math.abs(aEvent.clientY - this._startY) >= 1))
+ {
+ this._startX = this._startY = undefined;
+ return;
+ }
+
+ this._startX = this._startY = undefined;
+
+ aCallback.call(this, aEvent);
+ }, false);
+ },
+
+ /**
+ * Handler for the pref-changed event coming from the toolbox.
+ * Currently this function only handles the timestamps preferences.
+ *
+ * @private
+ * @param object aEvent
+ * This parameter is a string that holds the event name
+ * pref-changed in this case.
+ * @param object aData
+ * This is the pref-changed data object.
+ */
+ _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData)
+ {
+ if (aData.pref == PREF_MESSAGE_TIMESTAMP) {
+ if (aData.newValue) {
+ this.outputNode.classList.remove("hideTimestamps");
+ }
+ else {
+ this.outputNode.classList.add("hideTimestamps");
+ }
+ }
+ },
+
+ /**
+ * Copies the selected items to the system clipboard.
+ *
+ * @param object aOptions
+ * - linkOnly:
+ * An optional flag to copy only URL without timestamp and
+ * other meta-information. Default is false.
+ * - contextmenu:
+ * An optional flag to copy the last clicked item which brought
+ * up the context menu if nothing is selected. Default is false.
+ */
+ copySelectedItems: function WCF_copySelectedItems(aOptions)
+ {
+ aOptions = aOptions || { linkOnly: false, contextmenu: false };
+
+ // Gather up the selected items and concatenate their clipboard text.
+ let strings = [];
+
+ let children = this.output.getSelectedMessages();
+ if (!children.length && aOptions.contextmenu) {
+ children = [this._contextMenuHandler.lastClickedMessage];
+ }
+
+ for (let item of children) {
+ // Ensure the selected item hasn't been filtered by type or string.
+ if (!item.classList.contains("filtered-by-type") &&
+ !item.classList.contains("filtered-by-string")) {
+ let timestampString = l10n.timestampString(item.timestamp);
+ if (aOptions.linkOnly) {
+ strings.push(item.url);
+ }
+ else {
+ strings.push(item.clipboardText);
+ }
+ }
+ }
+
+ clipboardHelper.copyString(strings.join("\n"), this.document);
+ },
+
+ /**
+ * Object properties provider. This function gives you the properties of the
+ * remote object you want.
+ *
+ * @param string aActor
+ * The object actor ID from which you want the properties.
+ * @param function aCallback
+ * Function you want invoked once the properties are received.
+ */
+ objectPropertiesProvider:
+ function WCF_objectPropertiesProvider(aActor, aCallback)
+ {
+ this.webConsoleClient.inspectObjectProperties(aActor,
+ function(aResponse) {
+ if (aResponse.error) {
+ Cu.reportError("Failed to retrieve the object properties from the " +
+ "server. Error: " + aResponse.error);
+ return;
+ }
+ aCallback(aResponse.properties);
+ });
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string aActor
+ * The actor ID you want to release.
+ */
+ _releaseObject: function WCF__releaseObject(aActor)
+ {
+ if (this.proxy) {
+ this.proxy.releaseActor(aActor);
+ }
+ },
+
+ /**
+ * Open the selected item's URL in a new tab.
+ */
+ openSelectedItemInTab: function WCF_openSelectedItemInTab()
+ {
+ 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 WCF_destroy()
+ {
+ 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._repeatNodes = {};
+ this._outputQueue.forEach(this._destroyItem, this);
+ this._outputQueue = [];
+ this._itemDestroyQueue.forEach(this._destroyItem, this);
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this._networkRequests = {};
+
+ if (this._outputTimerInitialized) {
+ this._outputTimerInitialized = false;
+ this._outputTimer.cancel();
+ }
+ this._outputTimer = null;
+ if (this.jsterm) {
+ this.jsterm.destroy();
+ this.jsterm = null;
+ }
+ this.output.destroy();
+ this.output = null;
+
+ if (this._contextMenuHandler) {
+ this._contextMenuHandler.destroy();
+ this._contextMenuHandler = null;
+ }
+
+ this._commandController = null;
+
+ let onDestroy = () => {
+ this._destroyer.resolve(null);
+ };
+
+ if (this.proxy) {
+ this.proxy.disconnect().then(onDestroy);
+ this.proxy = null;
+ }
+ else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(aItem, aCurrentString)
+{
+ return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
+}
+
+
+
+/**
+ * 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 aWebConsoleFrame
+ * The WebConsoleFrame object that owns this JSTerm instance.
+ */
+function JSTerm(aWebConsoleFrame)
+{
+ this.hud = aWebConsoleFrame;
+ this.hudId = this.hud.hudId;
+
+ this.lastCompletion = { value: null };
+ this.history = [];
+
+ // Holds the number of entries in history. This value is incremented in
+ // this.execute().
+ this.historyIndex = 0; // incremented on this.execute()
+
+ // 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 = 0;
+ 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,
+
+ /**
+ * 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() this.hud.outputNode,
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() 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 JST_init()
+ {
+ let autocompleteOptions = {
+ onSelect: this.onAutocompleteSelect.bind(this),
+ onClick: this.acceptProposedCompletion.bind(this),
+ panelId: "webConsole_autocompletePopup",
+ listBoxId: "webConsole_autocompletePopupListBox",
+ position: "before_start",
+ theme: "auto",
+ direction: "ltr",
+ autoSelect: true
+ };
+ this.autocompletePopup = new AutocompletePopup(this.hud.document,
+ autocompleteOptions);
+
+ let doc = this.hud.document;
+ let inputContainer = doc.querySelector(".jsterm-input-container");
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+
+ if (this.hud.owner._browserConsole &&
+ !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+ inputContainer.style.display = "none";
+ }
+ else {
+ let okstring = l10n.getStr("selfxss.okstring");
+ let msg = l10n.getFormatStr("selfxss.msg", [okstring]);
+ this._onPaste = WebConsoleUtils.pasteHandlerGen(this.inputNode,
+ doc.getElementById("webconsole-notificationbox"),
+ msg, okstring);
+ this.inputNode.addEventListener("keypress", this._keyPress, false);
+ this.inputNode.addEventListener("paste", this._onPaste);
+ this.inputNode.addEventListener("drop", this._onPaste);
+ this.inputNode.addEventListener("input", this._inputEventHandler, false);
+ this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.addEventListener("focus", this._focusEventHandler, false);
+ }
+
+ this.hud.window.addEventListener("blur", this._blurEventHandler, false);
+ this.lastInputValue && this.setInputValue(this.lastInputValue);
+ },
+
+ /**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param object [aAfterMessage]
+ * Optional message after which the evaluation result will be
+ * inserted.
+ * @param function [aCallback]
+ * Optional function to invoke when the evaluation result is added to
+ * the output.
+ * @param object aResponse
+ * The message received from the server.
+ */
+ _executeResultCallback:
+ function JST__executeResultCallback(aAfterMessage, aCallback, aResponse)
+ {
+ if (!this.hud) {
+ return;
+ }
+ if (aResponse.error) {
+ Cu.reportError("Evaluation error " + aResponse.error + ": " +
+ aResponse.message);
+ return;
+ }
+ let errorMessage = aResponse.exceptionMessage;
+ let result = aResponse.result;
+ let helperResult = aResponse.helperResult;
+ let helperHasRawOutput = !!(helperResult || {}).rawOutput;
+
+ if (helperResult && helperResult.type) {
+ switch (helperResult.type) {
+ case "clearOutput":
+ this.clearOutput();
+ break;
+ case "inspectObject":
+ if (aAfterMessage) {
+ if (!aAfterMessage._objectActors) {
+ aAfterMessage._objectActors = new Set();
+ }
+ aAfterMessage._objectActors.add(helperResult.object.actor);
+ }
+ 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) {
+ aCallback && aCallback();
+ return;
+ }
+
+ let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage);
+ this.hud.output.addMessage(msg);
+
+ if (aCallback) {
+ let oldFlushCallback = this.hud._flushCallback;
+ this.hud._flushCallback = () => {
+ aCallback(msg.element);
+ if (oldFlushCallback) {
+ oldFlushCallback();
+ this.hud._flushCallback = oldFlushCallback;
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ msg._afterMessage = aAfterMessage;
+ msg._objectActors = new Set();
+
+ if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
+ msg._objectActors.add(aResponse.exception.actor);
+ }
+
+ if (WebConsoleUtils.isActorGrip(result)) {
+ msg._objectActors.add(result.actor);
+ }
+ },
+
+ /**
+ * Execute a string. Execution happens asynchronously in the content process.
+ *
+ * @param string [aExecuteString]
+ * The string you want to execute. If this is not provided, the current
+ * user input is used - taken from |this.inputNode.value|.
+ * @param function [aCallback]
+ * Optional function to invoke when the result is displayed.
+ * This is deprecated - please use the promise return value instead.
+ * @returns Promise
+ * Resolves with the message once the result is displayed.
+ */
+ execute: function JST_execute(aExecuteString, aCallback)
+ {
+ let deferred = promise.defer();
+ let callback = function(msg) {
+ deferred.resolve(msg);
+ if (aCallback) {
+ aCallback(msg);
+ }
+ }
+
+ // attempt to execute the content of the inputNode
+ aExecuteString = aExecuteString || this.inputNode.value;
+ if (!aExecuteString) {
+ return;
+ }
+
+ let selectedNodeActor = null;
+ let inspectorSelection = this.hud.owner.getInspectorSelection();
+ if (inspectorSelection) {
+ selectedNodeActor = inspectorSelection.nodeFront.actorID;
+ }
+
+ let message = new Messages.Simple(aExecuteString, {
+ category: "input",
+ severity: "log",
+ });
+ this.hud.output.addMessage(message);
+ let onResult = this._executeResultCallback.bind(this, message, callback);
+
+ let options = {
+ frame: this.SELECTED_FRAME,
+ selectedNodeActor: selectedNodeActor,
+ };
+
+ this.requestEvaluation(aExecuteString, 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++] = aExecuteString;
+ this.historyPlaceHolder = this.history.length;
+ WebConsoleUtils.usageCount++;
+ this.setInputValue("");
+ this.clearCompletion();
+ return deferred.promise;
+ },
+
+ /**
+ * Request a JavaScript string evaluation from the server.
+ *
+ * @param string aString
+ * String to execute.
+ * @param object [aOptions]
+ * 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 JST_requestEvaluation(aString, aOptions = {})
+ {
+ let deferred = promise.defer();
+
+ function onResult(aResponse) {
+ if (!aResponse.error) {
+ deferred.resolve(aResponse);
+ }
+ else {
+ deferred.reject(aResponse);
+ }
+ }
+
+ let frameActor = null;
+ if ("frame" in aOptions) {
+ frameActor = this.getFrameActor(aOptions.frame);
+ }
+
+ let evalOptions = {
+ bindObjectActor: aOptions.bindObjectActor,
+ frameActor: frameActor,
+ selectedNodeActor: aOptions.selectedNodeActor,
+ };
+
+ this.webConsoleClient.evaluateJSAsync(aString, onResult, evalOptions);
+ return deferred.promise;
+ },
+
+ /**
+ * Retrieve the FrameActor ID given a frame depth.
+ *
+ * @param number aFrame
+ * Frame depth.
+ * @return string|null
+ * The FrameActor ID for the given frame depth.
+ */
+ getFrameActor: function JST_getFrameActor(aFrame)
+ {
+ let state = this.hud.owner.getDebuggerFrames();
+ if (!state) {
+ return null;
+ }
+
+ let grip;
+ if (aFrame == this.SELECTED_FRAME) {
+ grip = state.frames[state.selected];
+ }
+ else {
+ grip = state.frames[aFrame];
+ }
+
+ return grip ? grip.actor : null;
+ },
+
+ /**
+ * Opens a new variables view that allows the inspection of the given object.
+ *
+ * @param object aOptions
+ * 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 JST_openVariablesView(aOptions)
+ {
+ let onContainerReady = (aWindow) => {
+ let container = aWindow.document.querySelector("#variables");
+ let view = this._variablesView;
+ if (!view || aOptions.targetElement) {
+ let viewOptions = {
+ container: container,
+ hideFilterInput: aOptions.hideFilterInput,
+ };
+ view = this._createVariablesView(viewOptions);
+ if (!aOptions.targetElement) {
+ this._variablesView = view;
+ aWindow.addEventListener("keypress", this._onKeypressInVariablesView);
+ }
+ }
+ aOptions.view = view;
+ this._updateVariablesView(aOptions);
+
+ if (!aOptions.targetElement && aOptions.autofocus) {
+ aWindow.focus();
+ }
+
+ this.emit("variablesview-open", view, aOptions);
+ return view;
+ };
+
+ let openPromise;
+ if (aOptions.targetElement) {
+ let deferred = promise.defer();
+ openPromise = deferred.promise;
+ let document = aOptions.targetElement.ownerDocument;
+ let iframe = document.createElementNS(XHTML_NS, "iframe");
+
+ iframe.addEventListener("load", function onIframeLoad(aEvent) {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ iframe.style.visibility = "visible";
+ deferred.resolve(iframe.contentWindow);
+ }, true);
+
+ iframe.flex = 1;
+ iframe.style.visibility = "hidden";
+ iframe.setAttribute("src", VARIABLES_VIEW_URL);
+ aOptions.targetElement.appendChild(iframe);
+ }
+ 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 JST__createSidebar()
+ {
+ let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
+ this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
+ this.sidebar.show();
+ },
+
+ /**
+ * Add the variables view tab to the sidebar.
+ *
+ * @private
+ * @return object
+ * A promise object for the adding of the new tab.
+ */
+ _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab()
+ {
+ 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, 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 aEvent
+ * The keypress DOM event object.
+ */
+ _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent)
+ {
+ let tag = aEvent.target.nodeName;
+ if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey ||
+ aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey ||
+ ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
+ return;
+ }
+
+ this._sidebarDestroy();
+ this.inputNode.focus();
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Create a variables view instance.
+ *
+ * @private
+ * @param object aOptions
+ * 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 JST__createVariablesView(aOptions)
+ {
+ let view = new VariablesView(aOptions.container);
+ view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
+ view.emptyText = l10n.getStr("emptyPropertiesList");
+ view.searchEnabled = !aOptions.hideFilterInput;
+ view.lazyEmpty = this._lazyVariablesView;
+
+ VariablesViewController.attach(view, {
+ getEnvironmentClient: aGrip => {
+ return new EnvironmentClient(this.hud.proxy.client, aGrip);
+ },
+ getObjectClient: aGrip => {
+ return new ObjectClient(this.hud.proxy.client, aGrip);
+ },
+ getLongStringClient: aGrip => {
+ return this.webConsoleClient.longString(aGrip);
+ },
+ releaseActor: aActor => {
+ this.hud._releaseObject(aActor);
+ },
+ simpleValueEvalMacro: simpleValueEvalMacro,
+ overrideValueEvalMacro: overrideValueEvalMacro,
+ getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+ });
+
+ // Relay events from the VariablesView.
+ view.on("fetched", (aEvent, aType, aVar) => {
+ this.emit("variablesview-fetched", aVar);
+ });
+
+ return view;
+ },
+
+ /**
+ * Update the variables view.
+ *
+ * @private
+ * @param object aOptions
+ * 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 JST__updateVariablesView(aOptions)
+ {
+ let view = aOptions.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(aActor => {
+ return view._consoleLastObjectActor != aActor;
+ });
+
+ if (aOptions.objectActor &&
+ (!this.hud.owner._browserConsole ||
+ Services.prefs.getBoolPref("devtools.chrome.enabled"))) {
+ // Make sure eval works in the correct context.
+ view.eval = this._variablesViewEvaluate.bind(this, aOptions);
+ view.switch = this._variablesViewSwitch.bind(this, aOptions);
+ view.delete = this._variablesViewDelete.bind(this, aOptions);
+ }
+ else {
+ view.eval = null;
+ view.switch = null;
+ view.delete = null;
+ }
+
+ let { variable, expanded } = view.controller.setSingleVariable(aOptions);
+ variable.evaluationMacro = simpleValueEvalMacro;
+
+ if (aOptions.objectActor) {
+ view._consoleLastObjectActor = aOptions.objectActor.actor;
+ }
+ else if (aOptions.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, aOptions);
+ });
+ },
+
+ /**
+ * The evaluation function used by the variables view when editing a property
+ * value.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param object aVar
+ * The Variable object instance for the edited property.
+ * @param string aValue
+ * The value the edited property was changed to.
+ */
+ _variablesViewEvaluate:
+ function JST__variablesViewEvaluate(aOptions, aVar, aValue)
+ {
+ let updater = this._updateVariablesView.bind(this, aOptions);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+ let string = aVar.evaluationMacro(aVar, aValue);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ this.requestEvaluation(string, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property deletion function used by the variables view when a property
+ * is deleted.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param object aVar
+ * The Variable object instance for the deleted property.
+ */
+ _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar)
+ {
+ let onEval = this._silentEvalCallback.bind(this, null);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions)
+ .then(onEval, onEval);
+ },
+
+ /**
+ * The property rename function used by the variables view when a property
+ * is renamed.
+ *
+ * @private
+ * @param object aOptions
+ * The options used for |this._updateVariablesView()|.
+ * @param object aVar
+ * The Variable object instance for the renamed property.
+ * @param string aNewName
+ * The new name for the property.
+ */
+ _variablesViewSwitch:
+ function JST__variablesViewSwitch(aOptions, aVar, aNewName)
+ {
+ let updater = this._updateVariablesView.bind(this, aOptions);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: aOptions.objectActor.actor,
+ };
+
+ let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]';
+ if (newSymbolicName == aVar.symbolicName) {
+ return;
+ }
+
+ let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" +
+ "delete _self" + aVar.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 aCallback
+ * Function to invoke once the response is received.
+ * @param object aResponse
+ * The response packet received from the server.
+ */
+ _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" +
+ aResponse.message);
+
+ aCallback && aCallback(aResponse);
+ return;
+ }
+
+ if (aResponse.exceptionMessage) {
+ let message = new Messages.Simple(aResponse.exceptionMessage, {
+ category: "output",
+ severity: "error",
+ timestamp: aResponse.timestamp,
+ });
+ this.hud.output.addMessage(message);
+ message._objectActors = new Set();
+ if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
+ message._objectActors.add(aResponse.exception.actor);
+ }
+ }
+
+ let helper = aResponse.helperResult || { type: null };
+ let helperGrip = null;
+ if (helper.type == "inspectObject") {
+ helperGrip = helper.object;
+ }
+
+ let grips = [aResponse.result, helperGrip];
+ for (let grip of grips) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this.hud._releaseObject(grip.actor);
+ }
+ }
+
+ aCallback && aCallback(aResponse);
+ },
+
+
+ /**
+ * Clear the Web Console output.
+ *
+ * This method emits the "messages-cleared" notification.
+ *
+ * @param boolean aClearStorage
+ * True if you want to clear the console messages storage associated to
+ * this Web Console.
+ */
+ clearOutput: function JST_clearOutput(aClearStorage)
+ {
+ 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 = [];
+ hud._networkRequests = {};
+ hud._repeatNodes = {};
+
+ if (aClearStorage) {
+ this.webConsoleClient.clearMessagesCache();
+ }
+
+ 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 JST_clearPrivateMessages()
+ {
+ 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 JST_resizeInput()
+ {
+ 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 aNewValue
+ * The new value to set.
+ * @returns void
+ */
+ setInputValue: function JST_setInputValue(aNewValue)
+ {
+ this.inputNode.value = aNewValue;
+ this.lastInputValue = aNewValue;
+ this.completeNode.value = "";
+ this.resizeInput();
+ this._inputChanged = true;
+ },
+
+ /**
+ * The inputNode "input" and "keyup" event handler.
+ * @private
+ */
+ _inputEventHandler: function JST__inputEventHandler()
+ {
+ if (this.lastInputValue != this.inputNode.value) {
+ this.resizeInput();
+ this.complete(this.COMPLETE_HINT_ONLY);
+ this.lastInputValue = this.inputNode.value;
+ this._inputChanged = true;
+ }
+ },
+
+ /**
+ * The window "blur" event handler.
+ * @private
+ */
+ _blurEventHandler: function JST__blurEventHandler()
+ {
+ if (this.autocompletePopup) {
+ this.clearCompletion();
+ }
+ },
+
+ /**
+ * The inputNode "keypress" event handler.
+ *
+ * @private
+ * @param nsIDOMEvent aEvent
+ */
+ _keyPress: function JST__keyPress(aEvent)
+ {
+ let inputNode = this.inputNode;
+ let inputUpdated = false;
+
+ if (aEvent.ctrlKey) {
+ switch (aEvent.charCode) {
+ case 101:
+ // control-e
+ if (Services.appinfo.OS == "WINNT") {
+ break;
+ }
+ let lineEndPos = inputNode.value.length;
+ if (this.hasMultilineInput()) {
+ // find index of closest newline >= cursor
+ for (let i = inputNode.selectionEnd; i<lineEndPos; i++) {
+ if (inputNode.value.charAt(i) == "\r" ||
+ inputNode.value.charAt(i) == "\n") {
+ lineEndPos = i;
+ break;
+ }
+ }
+ }
+ inputNode.setSelectionRange(lineEndPos, lineEndPos);
+ aEvent.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)) {
+ aEvent.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.
+ inputNode.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)) {
+ aEvent.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.
+ inputNode.focus();
+ }
+ this.clearCompletion();
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ else if (aEvent.shiftKey &&
+ aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+ // shift return
+ // TODO: expand the inputNode height by one line
+ return;
+ }
+
+ switch (aEvent.keyCode) {
+ case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ else if (this.sidebar) {
+ this._sidebarDestroy();
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
+ if (this._autocompletePopupNavigated &&
+ this.autocompletePopup.isOpen &&
+ this.autocompletePopup.selectedIndex > -1) {
+ this.acceptProposedCompletion();
+ }
+ else {
+ this.execute();
+ this._inputChanged = false;
+ }
+ aEvent.preventDefault();
+ break;
+
+ case Ci.nsIDOMKeyEvent.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) {
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.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) {
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEUP);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ }
+ else {
+ this.hud.outputNode.parentNode.scrollTop =
+ Math.max(0,
+ this.hud.outputNode.parentNode.scrollTop -
+ this.hud.outputNode.parentNode.clientHeight
+ );
+ }
+ aEvent.preventDefault();
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ }
+ else {
+ this.hud.outputNode.parentNode.scrollTop =
+ Math.min(this.hud.outputNode.parentNode.scrollHeight,
+ this.hud.outputNode.parentNode.scrollTop +
+ this.hud.outputNode.parentNode.clientHeight
+ );
+ }
+ aEvent.preventDefault();
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex = 0;
+ aEvent.preventDefault();
+ } else if (this.inputNode.value.length <= 0) {
+ this.hud.outputNode.parentNode.scrollTop = 0;
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_END:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex = this.autocompletePopup.itemCount - 1;
+ aEvent.preventDefault();
+ } else if (this.inputNode.value.length <= 0) {
+ this.hud.outputNode.parentNode.scrollTop = this.hud.outputNode.parentNode.scrollHeight;
+ aEvent.preventDefault();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
+ if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
+ this.clearCompletion();
+ }
+ break;
+
+ case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: {
+ let cursorAtTheEnd = this.inputNode.selectionStart ==
+ this.inputNode.selectionEnd &&
+ this.inputNode.selectionStart ==
+ this.inputNode.value.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()) {
+ aEvent.preventDefault();
+ }
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ }
+ break;
+ }
+ case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
+ // Generate a completion and accept the first proposed value.
+ if (this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion &&
+ this.acceptProposedCompletion()) {
+ aEvent.preventDefault();
+ }
+ else if (this._inputChanged) {
+ this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
+ aEvent.preventDefault();
+ }
+ break;
+ default:
+ break;
+ }
+ },
+
+ /**
+ * The inputNode "focus" event handler.
+ * @private
+ */
+ _focusEventHandler: function JST__focusEventHandler()
+ {
+ this._inputChanged = false;
+ },
+
+ /**
+ * Go up/down the history stack of input values.
+ *
+ * @param number aDirection
+ * History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
+ *
+ * @returns boolean
+ * True if the input value changed, false otherwise.
+ */
+ historyPeruse: function JST_historyPeruse(aDirection)
+ {
+ if (!this.history.length) {
+ return false;
+ }
+
+ // Up Arrow key
+ if (aDirection == 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.inputNode.value || "";
+ }
+
+ this.setInputValue(inputVal);
+ }
+ // Down Arrow key
+ else if (aDirection == HISTORY_FORWARD) {
+ 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 JST_hasMultilineInput()
+ {
+ return /[\r\n]/.test(this.inputNode.value);
+ },
+
+ /**
+ * 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 JST_canCaretGoPrevious()
+ {
+ 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 JST_canCaretGoNext()
+ {
+ 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 aType 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 inputNode.value is set to this value and the selection is set
+ * from the current cursor position to the end of the completed text.
+ * @param function aCallback
+ * 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 JSTF_complete(aType, aCallback)
+ {
+ let inputNode = this.inputNode;
+ let inputValue = inputNode.value;
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+
+ // If the inputNode has no value, then don't try to complete on it.
+ if (!inputValue) {
+ this.clearCompletion();
+ aCallback && aCallback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Only complete if the selection is empty.
+ if (inputNode.selectionStart != inputNode.selectionEnd) {
+ this.clearCompletion();
+ aCallback && aCallback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Update the completion results.
+ if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) {
+ this._updateCompletionResult(aType, aCallback);
+ return false;
+ }
+
+ let popup = this.autocompletePopup;
+ let accepted = false;
+
+ if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ accepted = true;
+ }
+ else if (aType == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ }
+ else if (aType == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ }
+ else if (aType == this.COMPLETE_PAGEUP) {
+ popup.selectPreviousPageItem();
+ }
+ else if (aType == this.COMPLETE_PAGEDOWN) {
+ popup.selectNextPageItem();
+ }
+
+ aCallback && aCallback(this);
+ this.emit("autocomplete-updated");
+ return accepted || popup.itemCount > 0;
+ },
+
+ /**
+ * Update the completion result. This operation is performed asynchronously by
+ * fetching updated results from the content process.
+ *
+ * @private
+ * @param int aType
+ * Completion type. See this.complete() for details.
+ * @param function [aCallback]
+ * Optional, function to invoke when completion results are received.
+ */
+ _updateCompletionResult:
+ function JST__updateCompletionResult(aType, aCallback)
+ {
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+ if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) {
+ return;
+ }
+
+ let requestId = gSequenceId();
+ let cursor = this.inputNode.selectionStart;
+ let input = this.inputNode.value.substring(0, cursor);
+ let cache = this._autocompleteCache;
+
+ // If the current input starts with the previous input, then we already
+ // have a list of suggestions and we just need to filter the cached
+ // suggestions. When the current input ends with a non-alphanumeric
+ // character we ask the server again for suggestions.
+
+ // Check if last character is non-alphanumeric
+ if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
+ this._autocompleteQuery = null;
+ this._autocompleteCache = null;
+ }
+
+ if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
+ let filterBy = input;
+ // Find the last non-alphanumeric other than _ or $ if it exists.
+ let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/);
+ // If input contains non-alphanumerics, use the part after the last one
+ // to filter the cache
+ if (lastNonAlpha) {
+ filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
+ }
+
+ let newList = cache.sort().filter(function(l) {
+ return l.startsWith(filterBy);
+ });
+
+ this.lastCompletion = {
+ requestId: null,
+ completionType: aType,
+ value: null,
+ };
+
+ let response = { matches: newList, matchProp: filterBy };
+ this._receiveAutocompleteProperties(null, aCallback, response);
+ return;
+ }
+
+ this._lastFrameActorId = frameActor;
+
+ this.lastCompletion = {
+ requestId: requestId,
+ completionType: aType,
+ value: null,
+ };
+
+ let callback = this._receiveAutocompleteProperties.bind(this, requestId,
+ aCallback);
+
+ this.webConsoleClient.autocomplete(input, cursor, callback, frameActor);
+ },
+
+ /**
+ * Handler for the autocompletion results. This method takes
+ * the completion result received from the server and updates the UI
+ * accordingly.
+ *
+ * @param number aRequestId
+ * Request ID.
+ * @param function [aCallback=null]
+ * Optional, function to invoke when the completion result is received.
+ * @param object aMessage
+ * The JSON message which holds the completion results received from
+ * the content process.
+ */
+ _receiveAutocompleteProperties:
+ function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage)
+ {
+ let inputNode = this.inputNode;
+ let inputValue = inputNode.value;
+ if (this.lastCompletion.value == inputValue ||
+ aRequestId != this.lastCompletion.requestId) {
+ return;
+ }
+ // Cache whatever came from the server if the last char is alphanumeric or '.'
+ let cursor = inputNode.selectionStart;
+ let inputUntilCursor = inputValue.substring(0, cursor);
+
+ if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) {
+ this._autocompleteCache = aMessage.matches;
+ this._autocompleteQuery = inputUntilCursor;
+ }
+
+ let matches = aMessage.matches;
+ let lastPart = aMessage.matchProp;
+ if (!matches.length) {
+ this.clearCompletion();
+ aCallback && aCallback(this);
+ this.emit("autocomplete-updated");
+ return;
+ }
+
+ let items = matches.reverse().map(function(aMatch) {
+ return { preLabel: lastPart, label: aMatch };
+ });
+
+ 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.inputNode.value.substr(0, this.inputNode.selectionStart);
+ let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
+ let x = offset * this.hud._inputCharWidth;
+ popup.openPopup(inputNode, x + this.hud._chevronWidth);
+ this._autocompletePopupNavigated = false;
+ }
+ else if (items.length < 2 && popup.isOpen) {
+ 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();
+ }
+
+ aCallback && aCallback(this);
+ this.emit("autocomplete-updated");
+ },
+
+ onAutocompleteSelect: function JSTF_onAutocompleteSelect()
+ {
+ // Render the suggestion only if the cursor is at the end of the input.
+ if (this.inputNode.selectionStart != this.inputNode.value.length) {
+ return;
+ }
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix = currentItem.label.substring(this.lastCompletion.
+ matchProp.length);
+ this.updateCompleteNode(suffix);
+ }
+ else {
+ this.updateCompleteNode("");
+ }
+ },
+
+ /**
+ * Clear the current completion information and close the autocomplete popup,
+ * if needed.
+ */
+ clearCompletion: function JSTF_clearCompletion()
+ {
+ this.autocompletePopup.clearItems();
+ this.lastCompletion = { value: null };
+ this.updateCompleteNode("");
+ if (this.autocompletePopup.isOpen) {
+ 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 JSTF_acceptProposedCompletion()
+ {
+ 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.inputNode.value;
+ this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor));
+ let newCursor = cursor + suffix.length;
+ this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
+ updated = true;
+ }
+
+ this.clearCompletion();
+
+ return updated;
+ },
+
+ /**
+ * Update the node that displays the currently selected autocomplete proposal.
+ *
+ * @param string aSuffix
+ * The proposed suffix for the inputNode value.
+ */
+ updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
+ {
+ // completion prefix = input, with non-control chars replaced by spaces
+ let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
+ this.completeNode.value = prefix + aSuffix;
+ },
+
+
+ /**
+ * Destroy the sidebar.
+ * @private
+ */
+ _sidebarDestroy: function JST__sidebarDestroy()
+ {
+ 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 JST_destroy()
+ {
+ this._sidebarDestroy();
+
+ this.clearCompletion();
+ this.clearOutput();
+
+ this.autocompletePopup.destroy();
+ this.autocompletePopup = null;
+
+ let popup = this.hud.owner.chromeWindow.document
+ .getElementById("webConsole_autocompletePopup");
+ if (popup) {
+ popup.parentNode.removeChild(popup);
+ }
+
+ if (this._onPaste) {
+ this.inputNode.removeEventListener("paste", this._onPaste, false);
+ this.inputNode.removeEventListener("drop", this._onPaste, false);
+ this._onPaste = null;
+ }
+
+ this.inputNode.removeEventListener("keypress", this._keyPress, false);
+ this.inputNode.removeEventListener("input", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
+ this.hud.window.removeEventListener("blur", this._blurEventHandler, false);
+
+ this.hud = null;
+ },
+};
+
+/**
+ * Utils: a collection of globally used functions.
+ */
+var Utils = {
+ /**
+ * Scrolls a node so that it's visible in its containing element.
+ *
+ * @param nsIDOMNode aNode
+ * The node to make visible.
+ * @returns void
+ */
+ scrollToVisible: function Utils_scrollToVisible(aNode)
+ {
+ aNode.scrollIntoView(false);
+ },
+
+ /**
+ * Check if the given output node is scrolled to the bottom.
+ *
+ * @param nsIDOMNode aOutputNode
+ * @return boolean
+ * True if the output node is scrolled to the bottom, or false
+ * otherwise.
+ */
+ isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode)
+ {
+ let lastNodeHeight = aOutputNode.lastChild ?
+ aOutputNode.lastChild.clientHeight : 0;
+ let scrollNode = aOutputNode.parentNode;
+ return scrollNode.scrollTop + scrollNode.clientHeight >=
+ scrollNode.scrollHeight - lastNodeHeight / 2;
+ },
+
+ /**
+ * Determine the category of a given nsIScriptError.
+ *
+ * @param nsIScriptError aScriptError
+ * 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 Utils_categoryForScriptError(aScriptError)
+ {
+ let category = aScriptError.category;
+
+ if (/^(?:CSS|Layout)\b/.test(category)) {
+ return CATEGORY_CSS;
+ }
+
+ switch (category) {
+ case "Mixed Content Blocker":
+ case "Mixed Content Message":
+ case "CSP":
+ case "Invalid HSTS Headers":
+ case "Invalid HPKP Headers":
+ case "SHA-1 Signature":
+ case "Insecure Password Field":
+ case "SSL":
+ case "CORS":
+ case "Iframe Sandbox":
+ case "Tracking Protection":
+ return CATEGORY_SECURITY;
+
+ default:
+ return CATEGORY_JS;
+ }
+ },
+
+ /**
+ * Retrieve the limit of messages for a specific category.
+ *
+ * @param number aCategory
+ * The category of messages you want to retrieve the limit for. See the
+ * CATEGORY_* constants.
+ * @return number
+ * The number of messages allowed for the specific category.
+ */
+ logLimitForCategory: function Utils_logLimitForCategory(aCategory)
+ {
+ let logLimit = DEFAULT_LOG_LIMIT;
+
+ try {
+ let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
+ logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
+ logLimit = Math.max(logLimit, 1);
+ }
+ catch (e) { }
+
+ return logLimit;
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// CommandController
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A controller (an instance of nsIController) that makes editing actions
+ * behave appropriately in the context of the Web Console.
+ */
+function CommandController(aWebConsole)
+{
+ this.owner = aWebConsole;
+}
+
+CommandController.prototype = {
+ /**
+ * Selects all the text in the HUD output.
+ */
+ selectAll: function CommandController_selectAll()
+ {
+ this.owner.output.selectAllMessages();
+ },
+
+ /**
+ * Open the URL of the selected message in a new tab.
+ */
+ openURL: function CommandController_openURL()
+ {
+ this.owner.openSelectedItemInTab();
+ },
+
+ copyURL: function CommandController_copyURL()
+ {
+ this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
+ },
+
+ /**
+ * Copies the last clicked message.
+ */
+ copyLastClicked: function CommandController_copy()
+ {
+ this.owner.copySelectedItems({ linkOnly: false, contextmenu: true });
+ },
+
+ supportsCommand: function CommandController_supportsCommand(aCommand)
+ {
+ if (!this.owner || !this.owner.output) {
+ return false;
+ }
+ return this.isCommandEnabled(aCommand);
+ },
+
+ isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "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 "consoleCmd_clearOutput":
+ case "cmd_selectAll":
+ case "cmd_find":
+ return true;
+ case "cmd_fontSizeEnlarge":
+ case "cmd_fontSizeReduce":
+ case "cmd_fontSizeReset":
+ case "cmd_close":
+ return this.owner.owner._browserConsole;
+ }
+ return false;
+ },
+
+ doCommand: function CommandController_doCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "consoleCmd_openURL":
+ this.openURL();
+ break;
+ case "consoleCmd_copyURL":
+ this.copyURL();
+ break;
+ case "consoleCmd_clearOutput":
+ this.owner.jsterm.clearOutput(true);
+ break;
+ case "cmd_copy":
+ this.copyLastClicked();
+ break;
+ case "cmd_find":
+ this.owner.filterBox.focus();
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ case "cmd_fontSizeEnlarge":
+ this.owner.changeFontSize("+");
+ break;
+ case "cmd_fontSizeReduce":
+ this.owner.changeFontSize("-");
+ break;
+ case "cmd_fontSizeReset":
+ this.owner.changeFontSize("");
+ break;
+ case "cmd_close":
+ this.owner.window.close();
+ 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 aWebConsole
+ * The Web Console instance that owns this connection proxy.
+ * @param RemoteTarget aTarget
+ * The target that the console will connect to.
+ */
+function WebConsoleConnectionProxy(aWebConsole, aTarget)
+{
+ this.owner = aWebConsole;
+ this.target = aTarget;
+
+ 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._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 instance.
+ *
+ * @see WebConsoleFrame
+ * @type object
+ */
+ owner: 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 WCCP_connect()
+ {
+ 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;
+
+ client.addListener("logMessage", this._onLogMessage);
+ client.addListener("pageError", this._onPageError);
+ client.addListener("consoleAPICall", this._onConsoleAPICall);
+ client.addListener("networkEvent", this._onNetworkEvent);
+ client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
+ client.addListener("fileActivity", this._onFileActivity);
+ client.addListener("reflowActivity", this._onReflowActivity);
+ client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
+ this.target.on("will-navigate", this._onTabNavigated);
+ this.target.on("navigate", this._onTabNavigated);
+
+ this._consoleActor = this.target.form.consoleActor;
+ if (!this.target.chrome) {
+ let tab = this.target.form;
+ this.owner.onLocationChange(tab.url, tab.title);
+ }
+ this._attachConsole();
+
+ return connPromise;
+ },
+
+ /**
+ * Connection timeout handler.
+ * @private
+ */
+ _connectionTimeout: function WCCP__connectionTimeout()
+ {
+ let error = {
+ error: "timeout",
+ message: l10n.getStr("connectionTimeout"),
+ };
+
+ this._connectDefer.reject(error);
+ },
+
+ /**
+ * Attach to the Web Console actor.
+ * @private
+ */
+ _attachConsole: function WCCP__attachConsole()
+ {
+ let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
+ "FileActivity"];
+ this.client.attachConsole(this._consoleActor, listeners,
+ this._onAttachConsole);
+ },
+
+ /**
+ * The "attachConsole" response handler.
+ *
+ * @private
+ * @param object aResponse
+ * The JSON response object received from the server.
+ * @param object aWebConsoleClient
+ * The WebConsoleClient instance for the attached console, for the
+ * specific tab we work with.
+ */
+ _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient)
+ {
+ if (aResponse.error) {
+ Cu.reportError("attachConsole failed: " + aResponse.error + " " +
+ aResponse.message);
+ this._connectDefer.reject(aResponse);
+ return;
+ }
+
+ this.webConsoleClient = aWebConsoleClient;
+
+ this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
+
+ let msgs = ["PageError", "ConsoleAPI"];
+ this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
+
+ this.owner._updateReflowActivityListener();
+ },
+
+ /**
+ * The "cachedMessages" response handler.
+ *
+ * @private
+ * @param object aResponse
+ * The JSON response object received from the server.
+ */
+ _onCachedMessages: function WCCP__onCachedMessages(aResponse)
+ {
+ if (aResponse.error) {
+ Cu.reportError("Web Console getCachedMessages error: " + aResponse.error +
+ " " + aResponse.message);
+ this._connectDefer.reject(aResponse);
+ return;
+ }
+
+ if (!this._connectTimer) {
+ // This happens if the promise is rejected (eg. a timeout), but the
+ // connection attempt is successful, nonetheless.
+ Cu.reportError("Web Console getCachedMessages error: invalid state.");
+ }
+
+ this.owner.displayCachedMessages(aResponse.messages);
+
+ if (!this._hasNativeConsoleAPI) {
+ this.owner.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 aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onPageError: function WCCP__onPageError(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handlePageError(aPacket.pageError);
+ }
+ },
+
+ /**
+ * The "logMessage" message type handler. We redirect any message to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onLogMessage: function WCCP__onLogMessage(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleLogMessage(aPacket);
+ }
+ },
+
+ /**
+ * The "consoleAPICall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleConsoleAPICall(aPacket.message);
+ }
+ },
+
+ /**
+ * The "networkEvent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleNetworkEvent(aPacket.eventActor);
+ }
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
+ {
+ if (this.owner) {
+ this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
+ aPacket);
+ }
+ },
+
+ /**
+ * The "fileActivity" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onFileActivity: function WCCP__onFileActivity(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleFileActivity(aPacket.uri);
+ }
+ },
+
+ _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.handleReflowActivity(aPacket);
+ }
+ },
+
+ /**
+ * The "lastPrivateContextExited" message type handler. When this message is
+ * received the Web Console UI is cleared.
+ *
+ * @private
+ * @param string aType
+ * Message type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onLastPrivateContextExited:
+ function WCCP__onLastPrivateContextExited(aType, aPacket)
+ {
+ if (this.owner && aPacket.from == this._consoleActor) {
+ this.owner.jsterm.clearPrivateMessages();
+ }
+ },
+
+ /**
+ * The "will-navigate" and "navigate" event handlers. We redirect any message
+ * to the UI for displaying.
+ *
+ * @private
+ * @param string aEvent
+ * Event type.
+ * @param object aPacket
+ * The message received from the server.
+ */
+ _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket)
+ {
+ if (!this.owner) {
+ return;
+ }
+
+ this.owner.handleTabNavigated(aEvent, aPacket);
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string aActor
+ * The actor ID to send the request to.
+ */
+ releaseActor: function WCCP_releaseActor(aActor)
+ {
+ if (this.client) {
+ this.client.release(aActor);
+ }
+ },
+
+ /**
+ * Disconnect the Web Console from the remote server.
+ *
+ * @return object
+ * A promise object that is resolved when disconnect completes.
+ */
+ disconnect: function WCCP_disconnect()
+ {
+ 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("networkEvent", this._onNetworkEvent);
+ this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
+ this.client.removeListener("fileActivity", this._onFileActivity);
+ this.client.removeListener("reflowActivity", this._onReflowActivity);
+ this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
+ this.target.off("will-navigate", this._onTabNavigated);
+ this.target.off("navigate", this._onTabNavigated);
+
+ this.client = null;
+ this.webConsoleClient = null;
+ this.target = null;
+ this.connected = false;
+ this.owner = null;
+ this._disconnecter.resolve(null);
+
+ return this._disconnecter.promise;
+ },
+};
+
+function gSequenceId()
+{
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+///////////////////////////////////////////////////////////////////////////////
+// Context Menu
+///////////////////////////////////////////////////////////////////////////////
+
+/*
+ * ConsoleContextMenu this used to handle the visibility of context menu items.
+ *
+ * @constructor
+ * @param object aOwner
+ * The WebConsoleFrame instance that owns this object.
+ */
+function ConsoleContextMenu(aOwner)
+{
+ this.owner = aOwner;
+ this.popup = this.owner.document.getElementById("output-contextmenu");
+ this.build = this.build.bind(this);
+ this.popup.addEventListener("popupshowing", this.build);
+}
+
+ConsoleContextMenu.prototype = {
+ lastClickedMessage: null,
+
+ /*
+ * Handle to show/hide context menu item.
+ */
+ build: function CCM_build(aEvent)
+ {
+ let metadata = this.getSelectionMetadata(aEvent.rangeParent);
+ for (let element of this.popup.children) {
+ element.hidden = this.shouldHideMenuItem(element, metadata);
+ }
+ },
+
+ /*
+ * Get selection information from the view.
+ *
+ * @param nsIDOMElement aClickElement
+ * The DOM element the user clicked on.
+ * @return object
+ * Selection metadata.
+ */
+ getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement)
+ {
+ let metadata = {
+ selectionType: "",
+ selection: new Set(),
+ };
+ let selectedItems = this.owner.output.getSelectedMessages();
+ if (!selectedItems.length) {
+ let clickedItem = this.owner.output.getMessageForElement(aClickElement);
+ if (clickedItem) {
+ this.lastClickedMessage = clickedItem;
+ selectedItems = [clickedItem];
+ }
+ }
+
+ metadata.selectionType = selectedItems.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;
+ }
+ }
+
+ return metadata;
+ },
+
+ /*
+ * Determine if an item should be hidden.
+ *
+ * @param nsIDOMElement aMenuItem
+ * @param object aMetadata
+ * @return boolean
+ * Whether the given item should be hidden or not.
+ */
+ shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata)
+ {
+ let selectionType = aMenuItem.getAttribute("selectiontype");
+ if (selectionType && !aMetadata.selectionType == selectionType) {
+ return true;
+ }
+
+ let selection = aMenuItem.getAttribute("selection");
+ if (!selection) {
+ return false;
+ }
+
+ let shouldHide = true;
+ let itemData = selection.split("|");
+ for (let type of aMetadata.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 CCM_destroy()
+ {
+ this.popup.removeEventListener("popupshowing", this.build);
+ this.popup = null;
+ this.owner = null;
+ this.lastClickedMessage = null;
+ },
+};
diff --git a/toolkit/devtools/webconsole/webconsole.xul b/toolkit/devtools/webconsole/webconsole.xul
new file mode 100644
index 000000000..2d5d85f6b
--- /dev/null
+++ b/toolkit/devtools/webconsole/webconsole.xul
@@ -0,0 +1,202 @@
+<?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://browser/locale/devtools/webConsole.dtd">
+%webConsoleDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/webconsole.css"
+ type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<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://browser/content/devtools/theme-switching.js"/>
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.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');"/>
+ <command id="consoleCmd_clearOutput"
+ oncommand="goDoCommand('consoleCmd_clearOutput');"/>
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find');"/>
+ <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');" disabled="true"/>
+ <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');" disabled="true"/>
+ <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');" disabled="true"/>
+ <command id="cmd_close" oncommand="goDoCommand('cmd_close');" disabled="true"/>
+ </commandset>
+ <keyset id="consoleKeys">
+ <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&findCmd.key;" command="cmd_find" modifiers="accel"/>
+ <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
+ <key key="&clearOutputCtrl.key;" command="consoleCmd_clearOutput" modifiers="control"/>
+ </keyset>
+ <keyset id="editMenuKeys"/>
+
+ <popupset id="mainPopupSet">
+ <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()">
+ <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;"
+ accesskey="&saveBodies.accesskey;"/>
+ <menuitem id="menu_openURL" label="&openURL.label;"
+ accesskey="&openURL.accesskey;" command="consoleCmd_openURL"
+ selection="network" selectionType="single"/>
+ <menuitem id="menu_copyURL" label="&copyURLCmd.label;"
+ accesskey="&copyURLCmd.accesskey;" command="consoleCmd_copyURL"
+ selection="network" selectionType="single"/>
+ <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">
+ <hbox class="devtools-toolbarbutton-group">
+ <toolbarbutton label="&btnPageNet.label;" type="menu-button"
+ category="net" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageNet.tooltip;"
+ accesskeyMacOSX="&btnPageNet.accesskeyMacOSX;"
+ accesskey="&btnPageNet.accesskey;"
+ tabindex="3">
+ <menupopup id="net-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="network"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox" autocheck="false"
+ prefKey="netwarn"/>
+ <menuitem label="&btnConsoleXhr;" type="checkbox" autocheck="false"
+ prefKey="netxhr"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="networkinfo"/>
+ <menuseparator id="saveBodiesSeparator" />
+ <menuitem id="saveBodies" type="checkbox" label="&saveBodies.label;"
+ accesskey="&saveBodies.accesskey;"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageCSS.label;" type="menu-button"
+ category="css" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageCSS.tooltip2;"
+ accesskey="&btnPageCSS.accesskey;"
+ tabindex="4">
+ <menupopup id="css-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="csserror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="cssparser"/>
+ <menuitem label="&btnConsoleReflows;" type="checkbox"
+ autocheck="false" prefKey="csslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageJS.label;" type="menu-button"
+ category="js" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageJS.tooltip;"
+ accesskey="&btnPageJS.accesskey;"
+ tabindex="5">
+ <menupopup id="js-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="exception"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="jswarn"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox"
+ autocheck="false" prefKey="jslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageSecurity.label;" type="menu-button"
+ category="security" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageSecurity.tooltip;"
+ accesskey="&btnPageSecurity.accesskey;"
+ tabindex="6">
+ <menupopup id="security-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="secerror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="secwarn"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageLogging.label;" type="menu-button"
+ category="logging" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageLogging.tooltip;"
+ accesskey="&btnPageLogging.accesskey3;"
+ tabindex="7">
+ <menupopup id="logging-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="error"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="warn"/>
+ <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false"
+ prefKey="info"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="log"/>
+ </menupopup>
+ </toolbarbutton>
+ </hbox>
+ <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
+ label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"
+ accesskey="&btnClear.accesskey;"
+ tabindex="8"/>
+
+ <spacer flex="1"/>
+
+ <textbox class="compact hud-filter-box devtools-searchinput" type="search"
+ placeholder="&filterOutput.placeholder;" tabindex="2"/>
+ </toolbar>
+
+ <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="output-container"
+ tabindex="0" role="document" aria-live="polite" />
+ </hbox>
+ <notificationbox id="webconsole-notificationbox">
+ <hbox class="jsterm-input-container" style="direction:ltr">
+ <stack class="jsterm-stack-node" flex="1">
+ <textbox class="jsterm-complete-node devtools-monospace"
+ multiline="true" rows="1" tabindex="-1"/>
+ <textbox class="jsterm-input-node devtools-monospace"
+ multiline="true" rows="1" tabindex="0"
+ aria-autocomplete="list"/>
+ </stack>
+ </hbox>
+ </notificationbox>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>