diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js index 4e93b1b8ffbd..599ee25c8559 100644 --- a/browser/devtools/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -487,11 +487,11 @@ var Scratchpad = { GetStringFromName("propertyPanel.updateButton.label"), accesskey: this.strings. GetStringFromName("propertyPanel.updateButton.accesskey"), - oncommand: function () { + oncommand: function _SP_PP_Update_onCommand() { let [error, result] = self.evalForContext(aEvalString); if (!error) { - propPanel.treeView.data = result; + propPanel.treeView.data = { object: result }; } } }); @@ -499,8 +499,9 @@ var Scratchpad = { let doc = this.browserWindow.document; let parent = doc.getElementById("mainPopupSet"); - let title = aOutputObject.toString(); - propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons); + let title = String(aOutputObject); + propPanel = new PropertyPanel(parent, title, { object: aOutputObject }, + buttons); let panel = propPanel.panel; panel.setAttribute("class", "scratchpad_propertyPanel"); diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js index b558f2798c6d..d38d0b3123bf 100644 --- a/browser/devtools/webconsole/HUDService-content.js +++ b/browser/devtools/webconsole/HUDService-content.js @@ -23,6 +23,7 @@ let Services = tempScope.Services; let gConsoleStorage = tempScope.ConsoleAPIStorage; let WebConsoleUtils = tempScope.WebConsoleUtils; let l10n = WebConsoleUtils.l10n; +let JSPropertyProvider = tempScope.JSPropertyProvider; tempScope = null; let _alive = true; // Track if this content script should still be alive. @@ -32,7 +33,6 @@ let _alive = true; // Track if this content script should still be alive. */ let Manager = { get window() content, - get console() this.window.console, sandbox: null, hudId: null, _sequence: 0, @@ -60,11 +60,7 @@ let Manager = { // Need to track the owner XUL window to listen to the unload and TabClose // events, to avoid memory leaks. - let xulWindow = this.window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .chromeEventHandler.ownerDocument.defaultView; - + let xulWindow = this._xulWindow(); xulWindow.addEventListener("unload", this._onXULWindowClose, false); let tabContainer = xulWindow.gBrowser.tabContainer; @@ -357,6 +353,19 @@ let Manager = { } }, + /** + * Find the XUL window that owns the content script. + * @private + * @return Window + * The XUL window that owns the content script. + */ + _xulWindow: function Manager__xulWindow() + { + return this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + }, + /** * Destroy the Web Console content script instance. */ @@ -366,11 +375,7 @@ let Manager = { Services.obs.removeObserver(this, "quit-application-granted"); _alive = false; - let xulWindow = this.window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .chromeEventHandler.ownerDocument.defaultView; - + let xulWindow = this._xulWindow(); xulWindow.removeEventListener("unload", this._onXULWindowClose, false); let tabContainer = xulWindow.gBrowser.tabContainer; tabContainer.removeEventListener("TabClose", this._onTabClose, false); @@ -389,11 +394,238 @@ let Manager = { }, }; +/** + * JSTerm helper functions. + * + * Defines a set of functions ("helper functions") that are available from the + * Web Console but not from the web page. + * + * A list of helper functions used by Firebug can be found here: + * http://getfirebug.com/wiki/index.php/Command_Line_API + */ +function JSTermHelper(aJSTerm) +{ + /** + * Find a node by ID. + * + * @param string aId + * The ID of the element you want. + * @return nsIDOMNode or null + * The result of calling document.getElementById(aId). + */ + aJSTerm.sandbox.$ = function JSTH_$(aId) + { + return aJSTerm.window.document.getElementById(aId); + }; + + /** + * Find the nodes matching a CSS selector. + * + * @param string aSelector + * A string that is passed to window.document.querySelectorAll. + * @return nsIDOMNodeList + * Returns the result of document.querySelectorAll(aSelector). + */ + aJSTerm.sandbox.$$ = function JSTH_$$(aSelector) + { + return aJSTerm.window.document.querySelectorAll(aSelector); + }; + + /** + * Runs an xPath query and returns all matched nodes. + * + * @param string aXPath + * xPath search query to execute. + * @param [optional] nsIDOMNode aContext + * Context to run the xPath query on. Uses window.document if not set. + * @returns array of nsIDOMNode + */ + aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext) + { + let nodes = []; + let doc = aJSTerm.window.document; + let aContext = aContext || doc; + + try { + let results = doc.evaluate(aXPath, aContext, null, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + let node; + while (node = results.iterateNext()) { + nodes.push(node); + } + } + catch (ex) { + aJSTerm.console.error(ex.message); + } + + return nodes; + }; + + /** + * Returns the currently selected object in the highlighter. + * + * Warning: this implementation crosses the process boundaries! This is not + * usable within a remote browser. To implement this feature correctly we need + * support for remote inspection capabilities within the Inspector as well. + * + * @return nsIDOMElement|null + * The DOM element currently selected in the highlighter. + */ + Object.defineProperty(aJSTerm.sandbox, "$0", { + get: function() { + try { + return Manager._xulWindow().InspectorUI.selection; + } + catch (ex) { + aJSTerm.console.error(ex.message); + } + }, + enumerable: true, + configurable: false + }); + + /** + * Clears the output of the JSTerm. + */ + aJSTerm.sandbox.clear = function JSTH_clear() + { + aJSTerm.helperEvaluated = true; + Manager.sendMessage("JSTerm:ClearOutput", {}); + }; + + /** + * Returns the result of Object.keys(aObject). + * + * @param object aObject + * Object to return the property names from. + * @returns array of string + */ + aJSTerm.sandbox.keys = function JSTH_keys(aObject) + { + return Object.keys(WebConsoleUtils.unwrap(aObject)); + }; + + /** + * Returns the values of all properties on aObject. + * + * @param object aObject + * Object to display the values from. + * @returns array of string + */ + aJSTerm.sandbox.values = function JSTH_values(aObject) + { + let arrValues = []; + let obj = WebConsoleUtils.unwrap(aObject); + + try { + for (let prop in obj) { + arrValues.push(obj[prop]); + } + } + catch (ex) { + aJSTerm.console.error(ex.message); + } + return arrValues; + }; + + /** + * Opens a help window in MDN. + */ + aJSTerm.sandbox.help = function JSTH_help() + { + aJSTerm.helperEvaluated = true; + aJSTerm.window.open( + "https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=" + + aJSTerm.window.navigator.language, "help", ""); + }; + + /** + * Inspects the passed aObject. This is done by opening the PropertyPanel. + * + * @param object aObject + * Object to inspect. + */ + aJSTerm.sandbox.inspect = function JSTH_inspect(aObject) + { + if (!WebConsoleUtils.isObjectInspectable(aObject)) { + return aObject; + } + + aJSTerm.helperEvaluated = true; + + let message = { + input: aJSTerm._evalInput, + objectCacheId: Manager.sequenceId, + }; + + message.resultObject = + aJSTerm.prepareObjectForRemote(WebConsoleUtils.unwrap(aObject), + message.objectCacheId); + + Manager.sendMessage("JSTerm:InspectObject", message); + }; + + /** + * Prints aObject to the output. + * + * @param object aObject + * Object to print to the output. + * @return string + */ + aJSTerm.sandbox.pprint = function JSTH_pprint(aObject) + { + aJSTerm.helperEvaluated = true; + if (aObject === null || aObject === undefined || aObject === true || + aObject === false) { + aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError")); + return; + } + else if (typeof aObject == "function") { + aJSTerm.helperRawOutput = true; + return aObject + "\n"; + } + + aJSTerm.helperRawOutput = true; + + let output = []; + let pairs = WebConsoleUtils.namesAndValuesOf(WebConsoleUtils.unwrap(aObject)); + pairs.forEach(function(aPair) { + output.push(aPair.name + ": " + aPair.value); + }); + + return " " + output.join("\n "); + }; + + /** + * Print a string to the output, as-is. + * + * @param string aString + * A string you want to output. + * @returns void + */ + aJSTerm.sandbox.print = function JSTH_print(aString) + { + aJSTerm.helperEvaluated = true; + aJSTerm.helperRawOutput = true; + return String(aString); + }; +} + /** * The JavaScript terminal is meant to allow remote code execution for the Web * Console. */ let JSTerm = { + get window() Manager.window, + get console() this.window.console, + + /** + * The Cu.Sandbox() object where code is evaluated. + */ + sandbox: null, + + _messageHandlers: {}, + /** * Evaluation result objects are cached in this object. The chrome process can * request any object based on its ID. @@ -406,16 +638,112 @@ let JSTerm = { init: function JST_init() { this._objectCache = {}; + this._messageHandlers = { + "JSTerm:EvalRequest": this.handleEvalRequest, + "JSTerm:GetEvalObject": this.handleGetEvalObject, + "JSTerm:Autocomplete": this.handleAutocomplete, + "JSTerm:ClearObjectCache": this.handleClearObjectCache, + }; - Manager.addMessageHandler("JSTerm:GetEvalObject", - this.handleGetEvalObject.bind(this)); - Manager.addMessageHandler("JSTerm:ClearObjectCache", - this.handleClearObjectCache.bind(this)); + for (let name in this._messageHandlers) { + let handler = this._messageHandlers[name].bind(this); + Manager.addMessageHandler(name, handler); + } + + this._createSandbox(); + }, + + /** + * Handler for the "JSTerm:EvalRequest" remote message. This method evaluates + * user input in the JavaScript sandbox and sends the result back to the + * remote process. The "JSTerm:EvalResult" message includes the following + * data: + * - id - the same ID as the EvalRequest (for tracking purposes). + * - input - the JS string that was evaluated. + * - resultString - the evaluation result converted to a string formatted + * for display. + * - timestamp - timestamp when evaluation occurred (Date.now(), + * milliseconds since the UNIX epoch). + * - inspectable - boolean that tells if the evaluation result object can be + * inspected or not. + * - error - the evaluation exception object (if any). + * - errorMessage - the exception object converted to a string (if any error + * occurred). + * - helperResult - boolean that tells if a JSTerm helper was evaluated. + * - helperRawOutput - boolean that tells if the helper evaluation result + * should be displayed as raw output. + * + * If the result object is inspectable then two additional properties are + * included: + * - childrenCacheId - tells where child objects are cached. This is the + * same as aRequest.resultCacheId. + * - resultObject - the result object prepared for the remote process. See + * this.prepareObjectForRemote(). + * + * @param object aRequest + * The code evaluation request object: + * - id - request ID. + * - str - string to evaluate. + * - resultCacheId - where to cache the evaluation child objects. + */ + handleEvalRequest: function JST_handleEvalRequest(aRequest) + { + let id = aRequest.id; + let input = aRequest.str; + let result, error = null; + let timestamp; + + this.helperEvaluated = false; + this.helperRawOutput = false; + this._evalInput = input; + try { + timestamp = Date.now(); + result = this.evalInSandbox(input); + } + catch (ex) { + error = ex; + } + delete this._evalInput; + + let inspectable = !error && WebConsoleUtils.isObjectInspectable(result); + let resultString = undefined; + if (!error) { + resultString = this.helperRawOutput ? result : + WebConsoleUtils.formatResult(result); + } + + let message = { + id: id, + input: input, + resultString: resultString, + timestamp: timestamp, + error: error, + errorMessage: error ? String(error) : null, + inspectable: inspectable, + helperResult: this.helperEvaluated, + helperRawOutput: this.helperRawOutput, + }; + + if (inspectable) { + message.childrenCacheId = aRequest.resultCacheId; + message.resultObject = + this.prepareObjectForRemote(result, message.childrenCacheId); + } + + Manager.sendMessage("JSTerm:EvalResult", message); }, /** * Handler for the remote "JSTerm:GetEvalObject" message. This allows the * remote Web Console instance to retrieve an object from the content process. + * The "JSTerm:EvalObject" message is sent back to the remote process: + * - id - the request ID, used to trace back to the initial request. + * - cacheId - the cache ID where the requested object is stored. + * - objectId - the ID of the object being sent. + * - object - the object representation prepared for remote inspection. See + * this.prepareObjectForRemote(). + * - childrenCacheId - the cache ID where any child object of |object| are + * stored. * * @param object aRequest * The message that requests the content object. Properties: cacheId, @@ -481,8 +809,7 @@ let JSTerm = { * method in aObject. Each element describes the property. For details * see WebConsoleUtils.namesAndValuesOf(). */ - prepareObjectForRemote: - function JST_prepareObjectForRemote(aObject, aCacheId) + prepareObjectForRemote: function JST_prepareObjectForRemote(aObject, aCacheId) { // Cache the properties that have inspectable values. let propCache = this._objectCache[aCacheId] || {}; @@ -494,14 +821,103 @@ let JSTerm = { return result; }, + /** + * Handler for the "JSTerm:Autocomplete" remote message. This handler provides + * completion results for user input. The "JSterm:AutocompleteProperties" + * message is sent to the remote process: + * - id - the same as request ID. + * - input - the user input (same as in the request message). + * - matches - an array of matched properties (strings). + * - matchProp - the part that was used from the user input for finding the + * matches. For details see the JSPropertyProvider description and + * implementation. + * + * + * @param object aRequest + * The remote request object which holds two properties: an |id| and + * the user |input|. + */ + handleAutocomplete: function JST_handleAutocomplete(aRequest) + { + let result = JSPropertyProvider(this.window, aRequest.input) || {}; + let message = { + id: aRequest.id, + input: aRequest.input, + matches: result.matches || [], + matchProp: result.matchProp, + }; + Manager.sendMessage("JSTerm:AutocompleteProperties", message); + }, + + /** + * Create the JavaScript sandbox where user input is evaluated. + * @private + */ + _createSandbox: function JST__createSandbox() + { + this.sandbox = new Cu.Sandbox(this.window, { + sandboxPrototype: this.window, + wantXrays: false, + }); + + this.sandbox.console = this.console; + + JSTermHelper(this); + }, + + /** + * Evaluates a string in the sandbox. + * + * @param string aString + * String to evaluate in the sandbox. + * @returns something + * The result of the evaluation. + */ + evalInSandbox: function JST_evalInSandbox(aString) + { + // The help function needs to be easy to guess, so we make the () optional + if (aString.trim() == "help" || aString.trim() == "?") { + aString = "help()"; + } + + let window = WebConsoleUtils.unwrap(this.sandbox.window); + let $ = null, $$ = null; + + // We prefer to execute the page-provided implementations for the $() and + // $$() functions. + if (typeof window.$ == "function") { + $ = this.sandbox.$; + delete this.sandbox.$; + } + if (typeof window.$$ == "function") { + $$ = this.sandbox.$$; + delete this.sandbox.$$; + } + + let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", + "Web Console", 1); + + if ($) { + this.sandbox.$ = $; + } + if ($$) { + this.sandbox.$$ = $$; + } + + return result; + }, + /** * Destroy the JSTerm instance. */ destroy: function JST_destroy() { - Manager.removeMessageHandler("JSTerm:GetEvalObject"); - Manager.removeMessageHandler("JSTerm:ClearObjectCache"); + for (let name in this._messageHandlers) { + Manager.removeMessageHandler(name); + } + delete this.sandbox; + delete this._messageHandlers; delete this._objectCache; }, }; diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index d1550638d43d..2837d6b514a7 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -16,10 +16,6 @@ Cu.import("resource:///modules/NetworkHelper.jsm"); var EXPORTED_SYMBOLS = ["HUDService", "ConsoleUtils"]; -XPCOMUtils.defineLazyServiceGetter(this, "scriptError", - "@mozilla.org/scripterror;1", - "nsIScriptError"); - XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor", "@mozilla.org/network/http-activity-distributor;1", "nsIHttpActivityDistributor"); @@ -57,24 +53,14 @@ XPCOMUtils.defineLazyGetter(this, "template", function () { }); XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () { - var obj = {}; - try { - Cu.import("resource:///modules/PropertyPanel.jsm", obj); - } catch (err) { - Cu.reportError(err); - } + let obj = {}; + Cu.import("resource:///modules/PropertyPanel.jsm", obj); return obj.PropertyPanel; }); -XPCOMUtils.defineLazyGetter(this, "PropertyPanelAsync", function () { +XPCOMUtils.defineLazyGetter(this, "PropertyTreeView", function () { let obj = {}; - Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj); - return obj.PropertyPanel; -}); - -XPCOMUtils.defineLazyGetter(this, "PropertyTreeViewAsync", function () { - let obj = {}; - Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj); + Cu.import("resource:///modules/PropertyPanel.jsm", obj); return obj.PropertyTreeView; }); @@ -100,12 +86,6 @@ XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () { return obj.ScratchpadManager; }); -XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () { - var obj = {}; - Cu.import("resource:///modules/PropertyPanel.jsm", obj); - return obj.namesAndValuesOf; -}); - XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () { let obj = {}; Cu.import("resource:///modules/WebConsoleUtils.jsm", obj); @@ -170,6 +150,7 @@ const LEVELS = { info: SEVERITY_INFO, log: SEVERITY_LOG, trace: SEVERITY_LOG, + debug: SEVERITY_LOG, dir: SEVERITY_LOG, group: SEVERITY_LOG, groupCollapsed: SEVERITY_LOG, @@ -1742,9 +1723,6 @@ HUD_SERVICE.prototype = // Remove children from the output. If the output is not cleared, there can // be leaks as some nodes has node.onclick = function; set and GC can't // remove the nodes then. - if (hud.jsterm) { - hud.jsterm.clearOutput(); - } if (hud.gcliterm) { hud.gcliterm.clearOutput(); } @@ -1770,10 +1748,6 @@ HUD_SERVICE.prototype = hud.splitter.parentNode.removeChild(hud.splitter); } - if (hud.jsterm) { - hud.jsterm.autocompletePopup.destroy(); - } - delete this.hudReferences[aHUDId]; for (let windowID in this.windowIds) { @@ -1816,7 +1790,7 @@ HUD_SERVICE.prototype = // begin observing HTTP traffic this.startHTTPObservation(); - HUDWindowObserver.init(); + WebConsoleObserver.init(); }, /** @@ -1840,7 +1814,7 @@ HUD_SERVICE.prototype = delete this.lastFinishedRequestCallback; - HUDWindowObserver.uninit(); + WebConsoleObserver.uninit(); }, /** @@ -2407,29 +2381,6 @@ HUD_SERVICE.prototype = return messageNode; }, - /** - * Initialize the JSTerm object to create a JS Workspace by attaching the UI - * into the given parent node, using the mixin. - * - * @param nsIDOMWindow aContext the context used for evaluating user input - * @param nsIDOMNode aParentNode where to attach the JSTerm - * @param object aConsole - * Console object used within the JSTerm instance to report errors - * and log data (by calling console.error(), console.log(), etc). - */ - initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode, aConsole) - { - // create Initial JS Workspace: - var context = Cu.getWeakReference(aContext); - - // Attach the UI into the target parent node using the mixin. - var firefoxMixin = new JSTermFirefoxMixin(context, aParentNode); - var jsTerm = new JSTerm(context, aParentNode, firefoxMixin, aConsole); - - // TODO: injection of additional functionality needs re-thinking/api - // see bug 559748 - }, - /** * Creates a generator that always returns a unique number for use in the * indexes @@ -2448,31 +2399,6 @@ HUD_SERVICE.prototype = return sequencer(aInt); }, - // See jsapi.h (JSErrorReport flags): - // http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.h#3429 - scriptErrorFlags: { - 0: "error", // JSREPORT_ERROR - 1: "warn", // JSREPORT_WARNING - 2: "exception", // JSREPORT_EXCEPTION - 4: "error", // JSREPORT_STRICT | JSREPORT_ERROR - 5: "warn", // JSREPORT_STRICT | JSREPORT_WARNING - 8: "error", // JSREPORT_STRICT_MODE_ERROR - 13: "warn", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR - }, - - /** - * replacement strings (L10N) - */ - scriptMsgLogLevel: { - 0: "typeError", // JSREPORT_ERROR - 1: "typeWarning", // JSREPORT_WARNING - 2: "typeException", // JSREPORT_EXCEPTION - 4: "typeError", // JSREPORT_STRICT | JSREPORT_ERROR - 5: "typeStrict", // JSREPORT_STRICT | JSREPORT_WARNING - 8: "typeError", // JSREPORT_STRICT_MODE_ERROR - 13: "typeWarning", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR - }, - /** * onTabClose event handler function * @@ -2891,20 +2817,15 @@ function HeadsUpDisplay(aConfig) // create a panel dynamically and attach to the parentNode this.createHUD(); - this.HUDBox.lastTimestamp = 0; + // create the JSTerm input element - try { - this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode); - if (this.jsterm) { - this.jsterm.inputNode.focus(); - } - if (this.gcliterm) { - this.gcliterm.inputNode.focus(); - } + this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode); + if (this.jsterm) { + this.jsterm.inputNode.focus(); } - catch (ex) { - Cu.reportError(ex); + else if (this.gcliterm) { + this.gcliterm.inputNode.focus(); } // A cache for tracking repeated CSS Nodes. @@ -2922,7 +2843,9 @@ HeadsUpDisplay.prototype = { * @type array */ _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI", - "WebConsole:CachedMessages", "WebConsole:PageError"], + "WebConsole:CachedMessages", "WebConsole:PageError", "JSTerm:EvalResult", + "JSTerm:AutocompleteProperties", "JSTerm:ClearOutput", + "JSTerm:InspectObject"], consolePanel: null, @@ -3246,10 +3169,8 @@ HeadsUpDisplay.prototype = { if (appName() == "FIREFOX") { if (!usegcli) { - let context = Cu.getWeakReference(aWindow); - let mixin = new JSTermFirefoxMixin(context, aParentNode, - aExistingConsole); - this.jsterm = new JSTerm(context, aParentNode, mixin, this.console); + let mixin = new JSTermFirefoxMixin(aParentNode, aExistingConsole); + this.jsterm = new JSTerm(this, mixin); } else { this.gcliterm = new GcliTerm(aWindow, this.hudId, this.chromeDocument, @@ -3321,15 +3242,10 @@ HeadsUpDisplay.prototype = { this.consolePanel.label = this.getPanelTitle(); } - if (this.jsterm) { - this.jsterm.context = Cu.getWeakReference(this.contentWindow); - this.jsterm.console = this.console; - this.jsterm.createSandbox(); - } - else if (this.gcliterm) { + if (this.gcliterm) { this.gcliterm.reattachConsole(this.contentWindow, this.console); } - else { + else if (!this.jsterm) { this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode); } }, @@ -3933,7 +3849,7 @@ HeadsUpDisplay.prototype = { data: { object: node._stacktrace }, }; - let propPanel = this.jsterm.openPropertyPanelAsync(options); + let propPanel = this.jsterm.openPropertyPanel(options); propPanel.panel.setAttribute("hudId", this.hudId); }.bind(this)); } @@ -4056,9 +3972,17 @@ HeadsUpDisplay.prototype = { } switch (aMessage.name) { + case "JSTerm:EvalResult": case "JSTerm:EvalObject": + case "JSTerm:AutocompleteProperties": this._receiveMessageWithCallback(aMessage.json); break; + case "JSTerm:ClearOutput": + this.jsterm.clearOutput(); + break; + case "JSTerm:InspectObject": + this.jsterm.handleInspectObject(aMessage.json); + break; case "WebConsole:ConsoleAPI": this.logConsoleAPIMessage(aMessage.json); break; @@ -4189,7 +4113,7 @@ HeadsUpDisplay.prototype = { */ destroy: function HUD_destroy() { - this.sendMessageToContent("WebConsole:Destroy", {hudId: this.hudId}); + this.sendMessageToContent("WebConsole:Destroy", {}); this._messageListeners.forEach(function(aName) { this.messageManager.removeMessageListener(aName, this); @@ -4246,532 +4170,34 @@ function NodeFactory(aFactoryType, ignored, aDocument) } } -////////////////////////////////////////////////////////////////////////// -// JS Completer -////////////////////////////////////////////////////////////////////////// - -const STATE_NORMAL = 0; -const STATE_QUOTE = 2; -const STATE_DQUOTE = 3; - -const OPEN_BODY = '{[('.split(''); -const CLOSE_BODY = '}])'.split(''); -const OPEN_CLOSE_BODY = { - '{': '}', - '[': ']', - '(': ')' -}; /** - * Analyses a given string to find the last statement that is interesting for - * later completion. + * 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. * - * @param string aStr - * A string to analyse. - * - * @returns object - * If there was an error in the string detected, then a object like - * - * { err: "ErrorMesssage" } - * - * is returned, otherwise a object like - * - * { - * state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE, - * startPos: index of where the last statement begins - * } - */ -function findCompletionBeginning(aStr) -{ - let bodyStack = []; - - let state = STATE_NORMAL; - let start = 0; - let c; - for (let i = 0; i < aStr.length; i++) { - c = aStr[i]; - - switch (state) { - // Normal JS state. - case STATE_NORMAL: - if (c == '"') { - state = STATE_DQUOTE; - } - else if (c == '\'') { - state = STATE_QUOTE; - } - else if (c == ';') { - start = i + 1; - } - else if (c == ' ') { - start = i + 1; - } - else if (OPEN_BODY.indexOf(c) != -1) { - bodyStack.push({ - token: c, - start: start - }); - start = i + 1; - } - else if (CLOSE_BODY.indexOf(c) != -1) { - var last = bodyStack.pop(); - if (!last || OPEN_CLOSE_BODY[last.token] != c) { - return { - err: "syntax error" - }; - } - if (c == '}') { - start = i + 1; - } - else { - start = last.start; - } - } - break; - - // Double quote state > " < - case STATE_DQUOTE: - if (c == '\\') { - i ++; - } - else if (c == '\n') { - return { - err: "unterminated string literal" - }; - } - else if (c == '"') { - state = STATE_NORMAL; - } - break; - - // Single quoate state > ' < - case STATE_QUOTE: - if (c == '\\') { - i ++; - } - else if (c == '\n') { - return { - err: "unterminated string literal" - }; - return; - } - else if (c == '\'') { - state = STATE_NORMAL; - } - break; - } - } - - return { - state: state, - startPos: start - }; -} - -/** - * Provides a list of properties, that are possible matches based on the passed - * scope and inputValue. - * - * @param object aScope - * Scope to use for the completion. - * - * @param string aInputValue - * Value that should be completed. - * - * @returns null or object - * If no completion valued could be computed, null is returned, - * otherwise a object with the following form is returned: - * { - * matches: [ string, string, string ], - * matchProp: Last part of the inputValue that was used to find - * the matches-strings. - * } - */ -function JSPropertyProvider(aScope, aInputValue) -{ - let obj = WebConsoleUtils.unwrap(aScope); - - // Analyse the aInputValue and find the beginning of the last part that - // should be completed. - let beginning = findCompletionBeginning(aInputValue); - - // There was an error analysing the string. - if (beginning.err) { - return null; - } - - // If the current state is not STATE_NORMAL, then we are inside of an string - // which means that no completion is possible. - if (beginning.state != STATE_NORMAL) { - return null; - } - - let completionPart = aInputValue.substring(beginning.startPos); - - // Don't complete on just an empty string. - if (completionPart.trim() == "") { - return null; - } - - let properties = completionPart.split('.'); - let matchProp; - if (properties.length > 1) { - matchProp = properties.pop().trimLeft(); - for (let i = 0; i < properties.length; i++) { - let prop = properties[i].trim(); - - // If obj is undefined or null, then there is no chance to run completion - // on it. Exit here. - if (typeof obj === "undefined" || obj === null) { - return null; - } - - // Check if prop is a getter function on obj. Functions can change other - // stuff so we can't execute them to get the next object. Stop here. - if (WebConsoleUtils.isNonNativeGetter(obj, prop)) { - return null; - } - try { - obj = obj[prop]; - } - catch (ex) { - return null; - } - } - } - else { - matchProp = properties[0].trimLeft(); - } - - // If obj is undefined or null, then there is no chance to run - // completion on it. Exit here. - if (typeof obj === "undefined" || obj === null) { - return null; - } - - // Skip Iterators and Generators. - if (WebConsoleUtils.isIteratorOrGenerator(obj)) { - return null; - } - - let matches = []; - for (let prop in obj) { - if (prop.indexOf(matchProp) == 0) { - matches.push(prop); - } - } - - return { - matchProp: matchProp, - matches: matches.sort(), - }; -} - -////////////////////////////////////////////////////////////////////////// -// JSTerm -////////////////////////////////////////////////////////////////////////// - -/** - * JSTermHelper - * - * Defines a set of functions ("helper functions") that are available from the - * WebConsole but not from the webpage. - * A list of helper functions used by Firebug can be found here: - * http://getfirebug.com/wiki/index.php/Command_Line_API - */ -function JSTermHelper(aJSTerm) -{ - /** - * Returns the result of document.getElementById(aId). - * - * @param string aId - * A string that is passed to window.document.getElementById. - * @returns nsIDOMNode or null - */ - aJSTerm.sandbox.$ = function JSTH_$(aId) - { - try { - return aJSTerm._window.document.getElementById(aId); - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - }; - - /** - * Returns the result of document.querySelectorAll(aSelector). - * - * @param string aSelector - * A string that is passed to window.document.querySelectorAll. - * @returns array of nsIDOMNode - */ - aJSTerm.sandbox.$$ = function JSTH_$$(aSelector) - { - try { - return aJSTerm._window.document.querySelectorAll(aSelector); - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - }; - - /** - * Runs a xPath query and returns all matched nodes. - * - * @param string aXPath - * xPath search query to execute. - * @param [optional] nsIDOMNode aContext - * Context to run the xPath query on. Uses window.document if not set. - * @returns array of nsIDOMNode - */ - aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext) - { - let nodes = []; - let doc = aJSTerm._window.document; - let aContext = aContext || doc; - - try { - let results = doc.evaluate(aXPath, aContext, null, - Ci.nsIDOMXPathResult.ANY_TYPE, null); - - let node; - while (node = results.iterateNext()) { - nodes.push(node); - } - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - - return nodes; - }; - - /** - * Returns the currently selected object in the highlighter. - * - * @returns nsIDOMNode or null - */ - Object.defineProperty(aJSTerm.sandbox, "$0", { - get: function() { - let mw = HUDService.currentContext(); - try { - return mw.InspectorUI.selection; - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - }, - enumerable: true, - configurable: false - }); - - /** - * Clears the output of the JSTerm. - */ - aJSTerm.sandbox.clear = function JSTH_clear() - { - aJSTerm.helperEvaluated = true; - aJSTerm.clearOutput(true); - }; - - /** - * Returns the result of Object.keys(aObject). - * - * @param object aObject - * Object to return the property names from. - * @returns array of string - */ - aJSTerm.sandbox.keys = function JSTH_keys(aObject) - { - try { - return Object.keys(WebConsoleUtils.unwrap(aObject)); - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - }; - - /** - * Returns the values of all properties on aObject. - * - * @param object aObject - * Object to display the values from. - * @returns array of string - */ - aJSTerm.sandbox.values = function JSTH_values(aObject) - { - let arrValues = []; - let obj = WebConsoleUtils.unwrap(aObject); - - try { - for (let prop in obj) { - arrValues.push(obj[prop]); - } - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - return arrValues; - }; - - /** - * Opens a help window in MDC - */ - aJSTerm.sandbox.help = function JSTH_help() - { - aJSTerm.helperEvaluated = true; - aJSTerm._window.open( - "https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=" + - aJSTerm._window.navigator.language, "help", ""); - }; - - /** - * Inspects the passed aObject. This is done by opening the PropertyPanel. - * - * @param object aObject - * Object to inspect. - * @returns void - */ - aJSTerm.sandbox.inspect = function JSTH_inspect(aObject) - { - aJSTerm.helperEvaluated = true; - let propPanel = aJSTerm.openPropertyPanel(null, - WebConsoleUtils.unwrap(aObject)); - propPanel.panel.setAttribute("hudId", aJSTerm.hudId); - }; - - aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode) - { - aJSTerm.helperEvaluated = true; - let doc = aJSTerm.inputNode.ownerDocument; - let win = doc.defaultView; - let panel = createElement(doc, "panel", { - label: "CSS Rules", - titlebar: "normal", - noautofocus: "true", - noautohide: "true", - close: "true", - width: 350, - height: (win.screen.height / 2) - }); - - let iframe = createAndAppendElement(panel, "iframe", { - src: "chrome://browser/content/devtools/cssruleview.xul", - flex: "1", - }); - - panel.addEventListener("load", function onLoad() { - panel.removeEventListener("load", onLoad, true); - let doc = iframe.contentDocument; - let view = new CssRuleView(doc); - doc.documentElement.appendChild(view.element); - view.highlight(aNode); - }, true); - - let parent = doc.getElementById("mainPopupSet"); - parent.appendChild(panel); - - panel.addEventListener("popuphidden", function onHide() { - panel.removeEventListener("popuphidden", onHide); - parent.removeChild(panel); - }); - - let footer = createElement(doc, "hbox", { align: "end" }); - createAndAppendElement(footer, "spacer", { flex: 1}); - createAndAppendElement(footer, "resizer", { dir: "bottomend" }); - panel.appendChild(footer); - - let anchor = win.gBrowser.selectedBrowser; - panel.openPopup(anchor, "end_before", 0, 0, false, false); - - } - - /** - * Prints aObject to the output. - * - * @param object aObject - * Object to print to the output. - * @returns void - */ - aJSTerm.sandbox.pprint = function JSTH_pprint(aObject) - { - aJSTerm.helperEvaluated = true; - if (aObject === null || aObject === undefined || aObject === true || aObject === false) { - aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError")); - return; - } - else if (typeof aObject === TYPEOF_FUNCTION) { - aJSTerm.writeOutput(aObject + "\n", CATEGORY_OUTPUT, SEVERITY_LOG); - return; - } - - let output = []; - let pairs = namesAndValuesOf(WebConsoleUtils.unwrap(aObject)); - - pairs.forEach(function(pair) { - output.push(" " + pair.display); - }); - - aJSTerm.writeOutput(output.join("\n"), CATEGORY_OUTPUT, SEVERITY_LOG); - }; - - /** - * Print a string to the output, as-is. - * - * @param string aString - * A string you want to output. - * @returns void - */ - aJSTerm.sandbox.print = function JSTH_print(aString) - { - aJSTerm.helperEvaluated = true; - aJSTerm.writeOutput("" + aString, CATEGORY_OUTPUT, SEVERITY_LOG); - }; -} - -/** - * JSTerm - * - * JavaScript Terminal: creates input nodes for console code interpretation - * and 'JS Workspaces' - */ - -/** - * Create a JSTerminal or attach a JSTerm input node to an existing output node, - * given by the parent node. - * - * @param object aContext - * Usually nsIDOMWindow, but doesn't have to be - * @param nsIDOMNode aParentNode where to attach the JSTerm + * @constructor + * @param object aHud + * The HeadsUpDisplay object that owns this JSTerm instance. * @param object aMixin * Gecko-app (or Jetpack) specific utility object - * @param object aConsole - * Console object to use within the JSTerm. */ -function JSTerm(aContext, aParentNode, aMixin, aConsole) +function JSTerm(aHud, aMixin) { - // set the context, attach the UI by appending to aParentNode + // attach the UI by appending to aParentNode this.application = appName(); - this.context = aContext; - this.parentNode = aParentNode; + this.hud = aHud; this.mixins = aMixin; - this.console = aConsole; - this.document = aParentNode.ownerDocument + this.document = this.hud.chromeDocument; - this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout; - - let node = aParentNode; - while (!node.hasAttribute("id")) { - node = node.parentNode; - } - this.hudId = node.getAttribute("id"); + this.hudId = this.hud.hudId; + this.lastCompletion = {}; this.history = []; this.historyIndex = 0; this.historyPlaceHolder = 0; // this.history.length; - this.log = LogFactory("*** JSTerm:"); - this.autocompletePopup = new AutocompletePopup(aParentNode.ownerDocument); + this.autocompletePopup = new AutocompletePopup(this.document); this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this); this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this); this.init(); @@ -4779,16 +4205,12 @@ function JSTerm(aContext, aParentNode, aMixin, aConsole) JSTerm.prototype = { lastInputValue: "", - propertyProvider: JSPropertyProvider, - COMPLETE_FORWARD: 0, COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, init: function JST_init() { - this.createSandbox(); - this.inputNode = this.mixins.inputNode; this.outputNode = this.mixins.outputNode; this.completeNode = this.mixins.completeNode; @@ -4819,62 +4241,82 @@ JSTerm.prototype = { this.mixins.attachUI(); }, - createSandbox: function JST_setupSandbox() - { - // create a JS Sandbox out of this.context - this.sandbox = new Cu.Sandbox(this._window, - { sandboxPrototype: this._window, wantXrays: false }); - this.sandbox.console = this.console; - JSTermHelper(this); - }, - get _window() + /** + * Asynchronously evaluate a string in the content process sandbox. + * + * @param string aString + * String to evaluate in the content process JavaScript sandbox. + * @param function [aCallback] + * Optional function to be invoked when the evaluation result is + * received. + */ + evalInContentSandbox: function JST_evalInContentSandbox(aString, aCallback) { - return this.context.get().QueryInterface(Ci.nsIDOMWindow); + let message = { + str: aString, + resultCacheId: "HUDEval-" + HUDService.sequenceId(), + }; + + this.hud.sendMessageToContent("JSTerm:EvalRequest", message, aCallback); + + return message; }, /** - * Evaluates a string in the sandbox. + * The "JSTerm:EvalResult" message handler. This is the JSTerm execution + * result callback which is invoked whenever JavaScript code evaluation + * results come from the content process. * - * @param string aString - * String to evaluate in the sandbox. - * @returns something - * The result of the evaluation. + * @private + * @param object aResponse + * The JSTerm:EvalResult message received from the content process. See + * JSTerm.handleEvalRequest() in HUDService-content.js for further + * details. + * @param object aRequest + * The JSTerm:EvalRequest message we sent to the content process. + * @see JSTerm.handleEvalRequest() in HUDService-content.js */ - evalInSandbox: function JST_evalInSandbox(aString) + _executeResultCallback: + function JST__executeResultCallback(aResponse, aRequest) { - // The help function needs to be easy to guess, so we make the () optional - if (aString.trim() === "help" || aString.trim() === "?") { - aString = "help()"; + let errorMessage = aResponse.errorMessage; + let resultString = aResponse.resultString; + + // Hide undefined results coming from JSTerm helper functions. + if (!errorMessage && + resultString == "undefined" && + aResponse.helperResult && + !aResponse.inspectable && + !aResponse.helperRawOutput) { + return; } - let window = WebConsoleUtils.unwrap(this.sandbox.window); - let $ = null, $$ = null; + let afterNode = aRequest.outputNode; - // We prefer to execute the page-provided implementations for the $() and - // $$() functions. - if (typeof window.$ == "function") { - $ = this.sandbox.$; - delete this.sandbox.$; + if (aResponse.errorMessage) { + this.writeOutput(aResponse.errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, + afterNode, aResponse.timestamp); } - if (typeof window.$$ == "function") { - $$ = this.sandbox.$$; - delete this.sandbox.$$; + else if (aResponse.inspectable) { + let node = this.writeOutputJS(aResponse.resultString, + this._evalOutputClick.bind(this, aResponse), + afterNode, aResponse.timestamp); + node._evalCacheId = aResponse.childrenCacheId; } - - let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1); - - if ($) { - this.sandbox.$ = $; + else { + this.writeOutput(aResponse.resultString, CATEGORY_OUTPUT, SEVERITY_LOG, + afterNode, aResponse.timestamp); } - if ($$) { - this.sandbox.$$ = $$; - } - - return result; }, - + /** + * 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|. + */ execute: function JST_execute(aExecuteString) { // attempt to execute the content of the inputNode @@ -4884,29 +4326,12 @@ JSTerm.prototype = { return; } - this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); + let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); - try { - this.helperEvaluated = false; - let result = this.evalInSandbox(aExecuteString); - - // Hide undefined results coming from helpers. - let shouldShow = !(result === undefined && this.helperEvaluated); - if (shouldShow) { - let inspectable = WebConsoleUtils.isObjectInspectable(result); - let resultString = WebConsoleUtils.formatResult(result); - - if (inspectable) { - this.writeOutputJS(aExecuteString, result, resultString); - } - else { - this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG); - } - } - } - catch (ex) { - this.writeOutput("" + ex, CATEGORY_OUTPUT, SEVERITY_ERROR); - } + let messageToContent = + this.evalInContentSandbox(aExecuteString, + this._executeResultCallback.bind(this)); + messageToContent.outputNode = node; this.history.push(aExecuteString); this.historyIndex++; @@ -4915,70 +4340,6 @@ JSTerm.prototype = { this.clearCompletion(); }, - /** - * Opens a new PropertyPanel. The panel has two buttons: "Update" reexecutes - * the passed aEvalString and places the result inside of the tree. The other - * button closes the panel. - * - * @param string aEvalString - * String that was used to eval the aOutputObject. Used as title - * and to update the tree content. - * @param object aOutputObject - * Object to display/inspect inside of the tree. - * @param nsIDOMNode aAnchor - * A node to popup the panel next to (using "after_pointer"). - * @returns object the created and opened propertyPanel. - */ - openPropertyPanel: function JST_openPropertyPanel(aEvalString, aOutputObject, - aAnchor) - { - let self = this; - let propPanel; - // The property panel has one button: - // `Update`: reexecutes the string executed on the command line. The - // result will be inspected by this panel. - let buttons = []; - - // If there is a evalString passed to this function, then add a `Update` - // button to the panel so that the evalString can be reexecuted to update - // the content of the panel. - if (aEvalString !== null) { - buttons.push({ - label: l10n.getStr("update.button"), - accesskey: l10n.getStr("update.accesskey"), - oncommand: function () { - try { - var result = self.evalInSandbox(aEvalString); - - if (result !== undefined) { - // TODO: This updates the value of the tree. - // However, the states of opened nodes is not saved. - // See bug 586246. - propPanel.treeView.data = result; - } - } - catch (ex) { - self.console.error(ex); - } - } - }); - } - - let doc = self.document; - let parent = doc.getElementById("mainPopupSet"); - let title = (aEvalString - ? l10n.getFormatStr("jsPropertyInspectTitle", [aEvalString]) - : l10n.getStr("jsPropertyTitle")); - - propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons); - propPanel.linkNode = aAnchor; - - let panel = propPanel.panel; - panel.openPopup(aAnchor, "after_pointer", 0, 0, false, false); - panel.sizeTo(350, 450); - return propPanel; - }, - /** * Opens a new property panel that allows the inspection of the given object. * The object information can be retrieved both async and sync, depending on @@ -4996,16 +4357,12 @@ JSTerm.prototype = { * not shown. * - data: * An object that represents the object you want to inspect. Please see - * the PropertyPanelAsync documentation - this object is passed to the - * PropertyPanelAsync constructor - * @param object aResponse - * The response object to display/inspect inside of the tree. - * @param nsIDOMNode aAnchor - * A node to popup the panel next to (using "after_pointer"). + * the PropertyPanel documentation - this object is passed to the + * PropertyPanel constructor * @return object - * The new instance of PropertyPanelAsync. + * The new instance of PropertyPanel. */ - openPropertyPanelAsync: function JST_openPropertyPanelAsync(aOptions) + openPropertyPanel: function JST_openPropertyPanel(aOptions) { // The property panel has one button: // `Update`: reexecutes the string executed on the command line. The @@ -5025,7 +4382,7 @@ JSTerm.prototype = { l10n.getFormatStr("jsPropertyInspectTitle", [aOptions.title]) : l10n.getStr("jsPropertyTitle"); - let propPanel = new PropertyPanelAsync(parent, title, aOptions.data, buttons); + let propPanel = new PropertyPanel(parent, title, aOptions.data, buttons); propPanel.panel.openPopup(aOptions.anchor, "after_pointer", 0, 0, false, false); propPanel.panel.sizeTo(350, 450); @@ -5042,52 +4399,32 @@ JSTerm.prototype = { }, /** - * Writes a JS object to the JSTerm outputNode. If the user clicks on the - * written object, openPropertyPanel is called to open up a panel to inspect - * the object. + * Writes a JS object to the JSTerm outputNode. * - * @param string aEvalString - * String that was evaluated to get the aOutputObject. - * @param object aResultObject - * The evaluation result object. - * @param object aOutputString - * The output string to be written to the outputNode. + * @param string aOutputMessage + * The message to display. + * @param function [aCallback] + * Optional function to invoke when users click the message. + * @param nsIDOMNode [aNodeAfter] + * Optional DOM node after which you want to insert the new message. + * This is used when execution results need to be inserted immediately + * after the user input. + * @param number [aTimestamp] + * Optional timestamp to show for the output message (millisconds since + * the UNIX epoch). If no timestamp is provided then Date.now() is + * used. + * @return nsIDOMNode + * The new message node. */ - writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject, aOutputString) + writeOutputJS: + function JST_writeOutputJS(aOutputMessage, aCallback, aNodeAfter, aTimestamp) { - let node = ConsoleUtils.createMessageNode(this.document, - CATEGORY_OUTPUT, - SEVERITY_LOG, - aOutputString, - this.hudId); - - let linkNode = node.querySelector(".webconsole-msg-body"); - - linkNode.classList.add("hud-clickable"); - linkNode.setAttribute("aria-haspopup", "true"); - - // Make the object bring up the property panel. - node.addEventListener("mousedown", function(aEvent) { - this._startX = aEvent.clientX; - this._startY = aEvent.clientY; - }, false); - - let self = this; - node.addEventListener("click", function(aEvent) { - if (aEvent.detail != 1 || aEvent.button != 0 || - (this._startX != aEvent.clientX && - this._startY != aEvent.clientY)) { - return; - } - - if (!this._panelOpen) { - let propPanel = self.openPropertyPanel(aEvalString, aOutputObject, this); - propPanel.panel.setAttribute("hudId", self.hudId); - this._panelOpen = true; - } - }, false); - - ConsoleUtils.outputMessageNode(node, this.hudId); + let node = this.writeOutput(aOutputMessage, CATEGORY_OUTPUT, SEVERITY_LOG, + aNodeAfter, aTimestamp); + if (aCallback) { + this.hud.makeOutputMessageLink(node, aCallback); + } + return node; }, /** @@ -5100,15 +4437,28 @@ JSTerm.prototype = { * The category of message: one of the CATEGORY_ constants. * @param number aSeverity * The severity of message: one of the SEVERITY_ constants. - * @returns void + * @param nsIDOMNode [aNodeAfter] + * Optional DOM node after which you want to insert the new message. + * This is used when execution results need to be inserted immediately + * after the user input. + * @param number [aTimestamp] + * Optional timestamp to show for the output message (millisconds since + * the UNIX epoch). If no timestamp is provided then Date.now() is + * used. + * @return nsIDOMNode + * The new message node. */ - writeOutput: function JST_writeOutput(aOutputMessage, aCategory, aSeverity) + writeOutput: + function JST_writeOutput(aOutputMessage, aCategory, aSeverity, aNodeAfter, + aTimestamp) { - let node = ConsoleUtils.createMessageNode(this.document, - aCategory, aSeverity, - aOutputMessage, this.hudId); + let node = ConsoleUtils.createMessageNode(this.document, aCategory, + aSeverity, aOutputMessage, + this.hudId, null, null, null, + null, aTimestamp); - ConsoleUtils.outputMessageNode(node, this.hudId); + ConsoleUtils.outputMessageNode(node, this.hudId, aNodeAfter); + return node; }, /** @@ -5399,7 +4749,7 @@ JSTerm.prototype = { * only if the selection/cursor is at the end of the string. If no completion * is found, the current inputNode value and cursor/selection stay. * - * @param int type possible values are + * @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 @@ -5417,11 +4767,13 @@ JSTerm.prototype = { * 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(type) + complete: function JSTF_complete(aType, aCallback) { let inputNode = this.inputNode; let inputValue = inputNode.value; @@ -5438,55 +4790,128 @@ JSTerm.prototype = { return false; } - let popup = this.autocompletePopup; - - if (!this.lastCompletion || this.lastCompletion.value != inputValue) { - let properties = this.propertyProvider(this.sandbox.window, inputValue); - if (!properties || !properties.matches.length) { - this.clearCompletion(); - return false; - } - - let items = properties.matches.map(function(aMatch) { - return {label: aMatch}; - }); - popup.setItems(items); - this.lastCompletion = {value: inputValue, - matchProp: properties.matchProp}; - - if (items.length > 1 && !popup.isOpen) { - popup.openPopup(this.inputNode); - } - else if (items.length < 2 && popup.isOpen) { - popup.hidePopup(); - } - - if (items.length == 1) { - popup.selectedIndex = 0; - } - this.onAutocompleteSelect(); + // Update the completion results. + if (this.lastCompletion.value != inputValue) { + this._updateCompletionResult(aType, aCallback); + return false; } + let popup = this.autocompletePopup; let accepted = false; - if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { this.acceptProposedCompletion(); accepted = true; } - else if (type == this.COMPLETE_BACKWARD) { - this.autocompletePopup.selectPreviousItem(); + else if (aType == this.COMPLETE_BACKWARD) { + popup.selectPreviousItem(); } - else if (type == this.COMPLETE_FORWARD) { - this.autocompletePopup.selectNextItem(); + else if (aType == this.COMPLETE_FORWARD) { + popup.selectNextItem(); } + aCallback && aCallback(this); 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) + { + if (this.lastCompletion.value == this.inputNode.value) { + return; + } + + let message = { + id: "HUDComplete-" + HUDService.sequenceId(), + input: this.inputNode.value, + }; + + this.lastCompletion = {requestId: message.id, completionType: aType}; + let callback = this._receiveAutocompleteProperties.bind(this, aCallback); + this.hud.sendMessageToContent("JSTerm:Autocomplete", message, callback); + }, + + /** + * Handler for the "JSTerm:AutocompleteProperties" message. This method takes + * the completion result received from the content process and updates the UI + * accordingly. + * + * @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(aCallback, aMessage) + { + let inputNode = this.inputNode; + let inputValue = inputNode.value; + if (aMessage.input != inputValue || + this.lastCompletion.value == inputValue || + aMessage.id != this.lastCompletion.requestId) { + return; + } + + let matches = aMessage.matches; + if (!matches.length) { + this.clearCompletion(); + return; + } + + let items = matches.map(function(aMatch) { + return { label: aMatch }; + }); + + let popup = this.autocompletePopup; + popup.setItems(items); + + let completionType = this.lastCompletion.completionType; + this.lastCompletion = { + value: inputValue, + matchProp: aMessage.matchProp, + }; + + if (items.length > 1 && !popup.isOpen) { + popup.openPopup(inputNode); + } + else if (items.length < 2 && popup.isOpen) { + popup.hidePopup(); + } + + 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); + }, + onAutocompleteSelect: function JSTF_onAutocompleteSelect() { let currentItem = this.autocompletePopup.selectedItem; - if (currentItem && this.lastCompletion) { + if (currentItem && this.lastCompletion.value) { let suffix = currentItem.label.substring(this.lastCompletion. matchProp.length); this.updateCompleteNode(suffix); @@ -5503,7 +4928,7 @@ JSTerm.prototype = { clearCompletion: function JSTF_clearCompletion() { this.autocompletePopup.clearItems(); - this.lastCompletion = null; + this.lastCompletion = {}; this.updateCompleteNode(""); if (this.autocompletePopup.isOpen) { this.autocompletePopup.hidePopup(); @@ -5522,7 +4947,7 @@ JSTerm.prototype = { let updated = false; let currentItem = this.autocompletePopup.selectedItem; - if (currentItem && this.lastCompletion) { + if (currentItem && this.lastCompletion.value) { let suffix = currentItem.label.substring(this.lastCompletion. matchProp.length); this.setInputValue(this.inputNode.value + suffix); @@ -5556,8 +4981,7 @@ JSTerm.prototype = { */ clearObjectCache: function JST_clearObjectCache(aCacheId) { - let hud = HUDService.getHudReferenceById(this.hudId); - hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId}); + this.hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId}); }, /** @@ -5584,8 +5008,147 @@ JSTerm.prototype = { resultCacheId: aResultCacheId, }; - let hud = HUDService.getHudReferenceById(this.hudId); - hud.sendMessageToContent("JSTerm:GetEvalObject", message, aCallback); + this.hud.sendMessageToContent("JSTerm:GetEvalObject", message, aCallback); + }, + + /** + * The "JSTerm:InspectObject" remote message handler. This allows the content + * process to open the Property Panel for a given object. + * + * @param object aRequest + * The request message from the content process. This message includes + * the user input string that was evaluated to inspect an object and + * the result object which is to be inspected. + */ + handleInspectObject: function JST_handleInspectObject(aRequest) + { + let options = { + title: aRequest.input, + + data: { + rootCacheId: aRequest.objectCacheId, + panelCacheId: aRequest.objectCacheId, + remoteObject: aRequest.resultObject, + remoteObjectProvider: this.remoteObjectProvider.bind(this), + }, + }; + + let propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + + let onPopupHide = function JST__onPopupHide() { + propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); + + this.clearObjectCache(options.data.panelCacheId); + }.bind(this); + + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }, + + /** + * The click event handler for evaluation results in the output. + * + * @private + * @param object aResponse + * The JSTerm:EvalResult message received from the content process. + * @param nsIDOMNode aLink + * The message node for which we are handling events. + */ + _evalOutputClick: function JST__evalOutputClick(aResponse, aLinkNode) + { + if (aLinkNode._panelOpen) { + return; + } + + let options = { + title: aResponse.input, + anchor: aLinkNode, + + // Data to inspect. + data: { + // This is where the resultObject children are cached. + rootCacheId: aResponse.childrenCacheId, + remoteObject: aResponse.resultObject, + // This is where all objects retrieved by the panel will be cached. + panelCacheId: "HUDPanel-" + HUDService.sequenceId(), + remoteObjectProvider: this.remoteObjectProvider.bind(this), + }, + }; + + options.updateButtonCallback = function JST__evalUpdateButton() { + this.evalInContentSandbox(aResponse.input, + this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, + aResponse)); + }.bind(this); + + let propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + + let onPopupHide = function JST__evalInspectPopupHide() { + propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); + + this.clearObjectCache(options.data.panelCacheId); + + if (!aLinkNode.parentNode && aLinkNode._evalCacheId) { + this.clearObjectCache(aLinkNode._evalCacheId); + } + }.bind(this); + + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }, + + /** + * The callback used for updating the Property Panel when the user clicks the + * Update button. + * + * @private + * @param object aOptions + * The options object used for opening the initial Property Panel. + * @param object aPropPanel + * The Property Panel instance. + * @param object aOldResponse + * The previous JSTerm:EvalResult message received from the content + * process. + * @param object aNewResponse + * The new JSTerm:EvalResult message received after the user clicked + * the Update button. + */ + _evalOutputUpdatePanelCallback: + function JST__updatePanelCallback(aOptions, aPropPanel, aOldResponse, + aNewResponse) + { + if (aNewResponse.errorMessage) { + this.writeOutput(aNewResponse.errorMessage, CATEGORY_OUTPUT, + SEVERITY_ERROR); + return; + } + + if (!aNewResponse.inspectable) { + this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR); + return; + } + + this.clearObjectCache(aOptions.data.panelCacheId); + this.clearObjectCache(aOptions.data.rootCacheId); + + if (aOptions.anchor && aOptions.anchor._evalCacheId) { + aOptions.anchor._evalCacheId = aNewResponse.childrenCacheId; + } + + // Update the old response object such that when the panel is reopen, the + // user sees the new response. + aOldResponse.id = aNewResponse.id; + aOldResponse.childrenCacheId = aNewResponse.childrenCacheId; + aOldResponse.resultObject = aNewResponse.resultObject; + aOldResponse.resultString = aNewResponse.resultString; + + aOptions.data.rootCacheId = aNewResponse.childrenCacheId; + aOptions.data.remoteObject = aNewResponse.resultObject; + + // TODO: This updates the value of the tree. + // However, the states of open nodes is not saved. + // See bug 586246. + aPropPanel.treeView.data = aOptions.data; }, /** @@ -5593,9 +5156,20 @@ JSTerm.prototype = { */ destroy: function JST_destroy() { + this.clearCompletion(); + this.clearOutput(); + + this.autocompletePopup.destroy(); + this.inputNode.removeEventListener("keypress", this._keyPress, false); this.inputNode.removeEventListener("input", this._inputEventHandler, false); this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); + + delete this.history; + delete this.hud; + delete this.autocompletePopup; + delete this.document; + delete this.mixins; }, }; @@ -5603,26 +5177,19 @@ JSTerm.prototype = { * Generates and attaches the JS Terminal part of the Web Console, which * essentially consists of the interactive JavaScript input facility. * - * @param nsWeakPtr aContext - * A weak pointer to the DOM window that contains the Web Console. * @param nsIDOMNode aParentNode * The Web Console wrapper node. * @param nsIDOMNode aExistingConsole * The Web Console output node. * @return void */ -function -JSTermFirefoxMixin(aContext, - aParentNode, - aExistingConsole) +function JSTermFirefoxMixin(aParentNode, aExistingConsole) { // aExisting Console is the existing outputNode to use in favor of // creating a new outputNode - this is so we can just attach the inputNode to // a normal HeadsUpDisplay console output, and re-use code. - this.context = aContext; this.parentNode = aParentNode; this.existingConsoleNode = aExistingConsole; - this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout; if (aParentNode.ownerDocument) { this.xulElementFactory = @@ -5895,7 +5462,7 @@ ConsoleUtils = { node.appendChild(bodyContainer); node.classList.add("webconsole-msg-inspector"); // Create the treeView object. - let treeView = node.propertyTreeView = new PropertyTreeViewAsync(); + let treeView = node.propertyTreeView = new PropertyTreeView(); treeView.data = { rootCacheId: body.cacheId, @@ -6132,8 +5699,11 @@ ConsoleUtils = { * The message node to send to the output. * @param string aHUDId * The ID of the HUD in which to insert this node. + * @param nsIDOMNode [aNodeAfter] + * Insert the node after the given aNodeAfter (optional). */ - outputMessageNode: function ConsoleUtils_outputMessageNode(aNode, aHUDId) { + outputMessageNode: + function ConsoleUtils_outputMessageNode(aNode, aHUDId, aNodeAfter) { ConsoleUtils.filterMessageNode(aNode, aHUDId); let outputNode = HUDService.hudReferences[aHUDId].outputNode; @@ -6152,7 +5722,7 @@ ConsoleUtils = { } if (!isRepeated) { - outputNode.appendChild(aNode); + outputNode.insertBefore(aNode, aNodeAfter ? aNodeAfter.nextSibling : null); } HUDService.regroupOutput(outputNode); @@ -6354,61 +5924,22 @@ HeadsUpDisplayUICommands = { } } }, - -}; - -/** - * A Console log entry - * - * @param JSObject aConfig, object literal with ConsolEntry properties - * @param integer aId - * @returns void - */ - -function ConsoleEntry(aConfig, id) -{ - if (!aConfig.logLevel && aConfig.message) { - throw new Error("Missing Arguments when creating a console entry"); - } - - this.config = aConfig; - this.id = id; - for (var prop in aConfig) { - if (!(typeof aConfig[prop] == "function")){ - this[prop] = aConfig[prop]; - } - } - - if (aConfig.logLevel == "network") { - this.transactions = { }; - if (aConfig.activity) { - this.transactions[aConfig.activity.stage] = aConfig.activity; - } - } - -} - -ConsoleEntry.prototype = { - - updateTransaction: function CE_updateTransaction(aActivity) { - this.transactions[aActivity.stage] = aActivity; - } }; ////////////////////////////////////////////////////////////////////////// -// HUDWindowObserver +// WebConsoleObserver ////////////////////////////////////////////////////////////////////////// -HUDWindowObserver = { +let WebConsoleObserver = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - init: function HWO_init() + init: function WCO_init() { Services.obs.addObserver(this, "content-document-global-created", false); Services.obs.addObserver(this, "quit-application-granted", false); }, - observe: function HWO_observe(aSubject, aTopic, aData) + observe: function WCO_observe(aSubject, aTopic) { if (aTopic == "content-document-global-created") { HUDService.windowInitializer(aSubject); @@ -6418,12 +5949,11 @@ HUDWindowObserver = { } }, - uninit: function HWO_uninit() + uninit: function WCO_uninit() { Services.obs.removeObserver(this, "content-document-global-created"); Services.obs.removeObserver(this, "quit-application-granted"); }, - }; /////////////////////////////////////////////////////////////////////////////// diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in index e9e8203dc7e3..a1a6359cb9b1 100644 --- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -12,7 +12,6 @@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ PropertyPanel.jsm \ - PropertyPanelAsync.jsm \ NetworkHelper.jsm \ AutocompletePopup.jsm \ WebConsoleUtils.jsm \ diff --git a/browser/devtools/webconsole/PropertyPanel.jsm b/browser/devtools/webconsole/PropertyPanel.jsm index 729f0f95c917..bac2dd34034c 100644 --- a/browser/devtools/webconsole/PropertyPanel.jsm +++ b/browser/devtools/webconsole/PropertyPanel.jsm @@ -4,6 +4,8 @@ * 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 = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; @@ -11,242 +13,18 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView", - "namesAndValuesOf", "isNonNativeGetter"]; +XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () { + let obj = {}; + Cu.import("resource:///modules/WebConsoleUtils.jsm", obj); + return obj.WebConsoleUtils; +}); -/////////////////////////////////////////////////////////////////////////// -//// Helper for PropertyTreeView +var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; -const TYPE_OBJECT = 0, TYPE_FUNCTION = 1, TYPE_ARRAY = 2, TYPE_OTHER = 3; - -/** - * Figures out the type of aObject and the string to display in the tree. - * - * @param object aObject - * The object to operate on. - * @returns object - * A object with the form: - * { - * type: TYPE_OBJECT || TYPE_FUNCTION || TYPE_ARRAY || TYPE_OTHER, - * display: string for displaying the object in the tree - * } - */ -function presentableValueFor(aObject) -{ - if (aObject === null || aObject === undefined) { - return { - type: TYPE_OTHER, - display: aObject === undefined ? "undefined" : "null" - }; - } - - let presentable; - switch (aObject.constructor && aObject.constructor.name) { - case "Array": - return { - type: TYPE_ARRAY, - display: "Array" - }; - - case "String": - return { - type: TYPE_OTHER, - display: "\"" + aObject + "\"" - }; - - case "Date": - case "RegExp": - case "Number": - case "Boolean": - return { - type: TYPE_OTHER, - display: aObject - }; - - case "Iterator": - return { - type: TYPE_OTHER, - display: "Iterator" - }; - - case "Function": - presentable = aObject.toString(); - return { - type: TYPE_FUNCTION, - display: presentable.substring(0, presentable.indexOf(')') + 1) - }; - - default: - presentable = aObject.toString(); - let m = /^\[object (\S+)\]/.exec(presentable); - - try { - if (typeof aObject == "object" && typeof aObject.next == "function" && - m && m[1] == "Generator") { - return { - type: TYPE_OTHER, - display: m[1] - }; - } - } - catch (ex) { - // window.history.next throws in the typeof check above. - return { - type: TYPE_OBJECT, - display: m ? m[1] : "Object" - }; - } - - if (typeof aObject == "object" && typeof aObject.__iterator__ == "function") { - return { - type: TYPE_OTHER, - display: "Iterator" - }; - } - - return { - type: TYPE_OBJECT, - display: m ? m[1] : "Object" - }; - } -} - -/** - * Tells if the given function is native or not. - * - * @param function aFunction - * The function you want to check if it is native or not. - * - * @return boolean - * True if the given function is native, false otherwise. - */ -function isNativeFunction(aFunction) -{ - return typeof aFunction == "function" && !("prototype" in aFunction); -} - -/** - * Tells if the given property of the provided object is a non-native getter or - * not. - * - * @param object aObject - * The object that contains the property. - * - * @param string aProp - * The property you want to check if it is a getter or not. - * - * @return boolean - * True if the given property is a getter, false otherwise. - */ -function isNonNativeGetter(aObject, aProp) { - if (typeof aObject != "object") { - return false; - } - let desc; - while (aObject) { - try { - if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) { - break; - } - } - catch (ex) { - // Native getters throw here. See bug 520882. - if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || - ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") { - return false; - } - throw ex; - } - aObject = Object.getPrototypeOf(aObject); - } - if (desc && desc.get && !isNativeFunction(desc.get)) { - return true; - } - return false; -} - -/** - * Get an array of property name value pairs for the tree. - * - * @param object aObject - * The object to get properties for. - * @returns array of object - * Objects have the name, value, display, type, children properties. - */ -function namesAndValuesOf(aObject) -{ - let pairs = []; - let value, presentable; - - let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; - - for (var propName in aObject) { - // See bug 632275: skip deprecated width and height properties. - if (isDOMDocument && (propName == "width" || propName == "height")) { - continue; - } - - // Also skip non-native getters. - if (isNonNativeGetter(aObject, propName)) { - value = ""; // Value is never displayed. - presentable = {type: TYPE_OTHER, display: "Getter"}; - } - else { - try { - value = aObject[propName]; - presentable = presentableValueFor(value); - } - catch (ex) { - continue; - } - } - - let pair = {}; - pair.name = propName; - pair.display = propName + ": " + presentable.display; - pair.type = presentable.type; - pair.value = value; - - // Convert the pair.name to a number for later sorting. - pair.nameNumber = parseFloat(pair.name) - if (isNaN(pair.nameNumber)) { - pair.nameNumber = false; - } - - pairs.push(pair); - } - - pairs.sort(function(a, b) - { - // Sort numbers. - if (a.nameNumber !== false && b.nameNumber === false) { - return -1; - } - else if (a.nameNumber === false && b.nameNumber !== false) { - return 1; - } - else if (a.nameNumber !== false && b.nameNumber !== false) { - return a.nameNumber - b.nameNumber; - } - // Sort string. - else if (a.name < b.name) { - return -1; - } - else if (a.name > b.name) { - return 1; - } - else { - return 0; - } - }); - - return pairs; -} /////////////////////////////////////////////////////////////////////////// //// PropertyTreeView. - /** * This is an implementation of the nsITreeView interface. For comments on the * interface properties, see the documentation: @@ -254,30 +32,87 @@ function namesAndValuesOf(aObject) */ var PropertyTreeView = function() { this._rows = []; + this._objectCache = {}; }; PropertyTreeView.prototype = { - /** * Stores the visible rows of the tree. + * @private */ _rows: null, /** * Stores the nsITreeBoxObject for this tree. + * @private */ _treeBox: null, + /** + * Stores cached information about local objects being inspected. + * @private + */ + _objectCache: null, + /** * Use this setter to update the content of the tree. * - * @param object aObject - * The new object to be displayed in the tree. - * @returns void + * @param object aData + * A meta object that holds information about the object you want to + * display in the property panel. Object properties: + * - object: + * This is the raw object you want to display. You can only provide + * this object if you want the property panel to work in sync mode. + * - remoteObject: + * An array that holds information on the remote object being + * inspected. Each element in this array describes each property in the + * remote object. See WebConsoleUtils.namesAndValuesOf() for details. + * - rootCacheId: + * The cache ID where the objects referenced in remoteObject are found. + * - panelCacheId: + * The cache ID where any object retrieved by this property panel + * instance should be stored into. + * - remoteObjectProvider: + * A function that is invoked when a new object is needed. This is + * called when the user tries to expand an inspectable property. The + * callback must take four arguments: + * - fromCacheId: + * Tells from where to retrieve the object the user picked (from + * which cache ID). + * - objectId: + * The object ID the user wants. + * - panelCacheId: + * Tells in which cache ID to store the objects referenced by + * objectId so they can be retrieved later. + * - callback: + * The callback function to be invoked when the remote object is + * received. This function takes one argument: the raw message + * received from the Web Console content script. */ - set data(aObject) { + set data(aData) { let oldLen = this._rows.length; - this._rows = this.getChildItems(aObject, true); + + this._cleanup(); + + if (!aData) { + return; + } + + if (aData.remoteObject) { + this._rootCacheId = aData.rootCacheId; + this._panelCacheId = aData.panelCacheId; + this._remoteObjectProvider = aData.remoteObjectProvider; + this._rows = [].concat(aData.remoteObject); + this._updateRemoteObject(this._rows, 0); + } + else if (aData.object) { + this._rows = this._inspectObject(aData.object); + } + else { + throw new Error("First argument must have a .remoteObject or " + + "an .object property!"); + } + if (this._treeBox) { this._treeBox.beginUpdateBatch(); if (oldLen) { @@ -289,53 +124,66 @@ PropertyTreeView.prototype = { }, /** - * Generates the child items for the treeView of a given aItem. If there is - * already a children property on the aItem, this cached one is returned. + * Update a remote object so it can be used with the tree view. This method + * adds properties to each array element. * - * @param object aItem - * An item of the tree's elements to generate the children for. - * @param boolean aRootElement - * If set, aItem is handled as an JS object and not as an item - * element of the tree. - * @returns array of objects - * Child items of aItem. + * @private + * @param array aObject + * The remote object you want prepared for use with the tree view. + * @param number aLevel + * The level you want to give to each property in the remote object. */ - getChildItems: function(aItem, aRootElement) + _updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel) { - // If item.children is an array, then the children has already been - // computed and can get returned directly. - // Skip this checking if aRootElement is true. It could happen, that aItem - // is passed as ({children:[1,2,3]}) which would be true, although these - // "kind" of children has no value/type etc. data as needed to display in - // the tree. As the passed ({children:[1,2,3]}) are instanceof - // itsWindow.Array and not this modules's global Array - // aItem.children instanceof Array can't be true, but for saftey the - // !aRootElement is kept here. - if (!aRootElement && aItem && aItem.children instanceof Array) { - return aItem.children; - } + aObject.forEach(function(aElement) { + aElement.level = aLevel; + aElement.isOpened = false; + aElement.children = null; + }); + }, - let pairs; - let newPairLevel; + /** + * Inspect a local object. + * + * @private + * @param object aObject + * The object you want to inspect. + */ + _inspectObject: function PTV__inspectObject(aObject) + { + this._objectCache = {}; + this._remoteObjectProvider = this._localObjectProvider.bind(this); + let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache); + this._updateRemoteObject(children, 0); + return children; + }, - if (!aRootElement) { - newPairLevel = aItem.level + 1; - aItem = aItem.value; - } - else { - newPairLevel = 0; - } - - pairs = namesAndValuesOf(aItem); - - for each (var pair in pairs) { - pair.level = newPairLevel; - pair.isOpened = false; - pair.children = pair.type == TYPE_OBJECT || pair.type == TYPE_FUNCTION || - pair.type == TYPE_ARRAY; - } - - return pairs; + /** + * An object provider for when the user inspects local objects (not remote + * ones). + * + * @private + * @param string aFromCacheId + * The cache ID from where to retrieve the desired object. + * @param string aObjectId + * The ID of the object you want. + * @param string aDestCacheId + * The ID of the cache where to store any objects referenced by the + * desired object. + * @param function aCallback + * The function you want to receive the object. + */ + _localObjectProvider: + function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId, + aCallback) + { + let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId], + this._objectCache); + aCallback({cacheId: aFromCacheId, + objectId: aObjectId, + object: object, + childrenCacheId: aDestCacheId || aFromCacheId, + }); }, /** nsITreeView interface implementation **/ @@ -344,10 +192,19 @@ PropertyTreeView.prototype = { get rowCount() { return this._rows.length; }, setTree: function(treeBox) { this._treeBox = treeBox; }, - getCellText: function(idx, column) { return this._rows[idx].display; }, - getLevel: function(idx) { return this._rows[idx].level; }, - isContainer: function(idx) { return !!this._rows[idx].children; }, - isContainerOpen: function(idx) { return this._rows[idx].isOpened; }, + getCellText: function(idx, column) { + let row = this._rows[idx]; + return row.name + ": " + row.value; + }, + getLevel: function(idx) { + return this._rows[idx].level; + }, + isContainer: function(idx) { + return !!this._rows[idx].inspectable; + }, + isContainerOpen: function(idx) { + return this._rows[idx].isOpened; + }, isContainerEmpty: function(idx) { return false; }, isSeparator: function(idx) { return false; }, isSorted: function() { return false; }, @@ -359,7 +216,7 @@ PropertyTreeView.prototype = { if (this.getLevel(idx) == 0) { return -1; } - for (var t = idx - 1; t >= 0 ; t--) { + for (var t = idx - 1; t >= 0; t--) { if (this.isContainer(t)) { return t; } @@ -375,13 +232,13 @@ PropertyTreeView.prototype = { toggleOpenState: function(idx) { - var item = this._rows[idx]; - if (!item.children) { + let item = this._rows[idx]; + if (!item.inspectable) { return; } - this._treeBox.beginUpdateBatch(); if (item.isOpened) { + this._treeBox.beginUpdateBatch(); item.isOpened = false; var thisLevel = item.level; @@ -394,18 +251,38 @@ PropertyTreeView.prototype = { this._rows.splice(idx + 1, deleteCount); this._treeBox.rowCountChanged(idx + 1, -deleteCount); } + this._treeBox.invalidateRow(idx); + this._treeBox.endUpdateBatch(); } else { - item.isOpened = true; + let levelUpdate = true; + let callback = function _onRemoteResponse(aResponse) { + this._treeBox.beginUpdateBatch(); + item.isOpened = true; - var toInsert = this.getChildItems(item); - item.children = toInsert; - this._rows.splice.apply(this._rows, [idx + 1, 0].concat(toInsert)); + if (levelUpdate) { + this._updateRemoteObject(aResponse.object, item.level + 1); + item.children = aResponse.object; + } - this._treeBox.rowCountChanged(idx + 1, toInsert.length); + this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children)); + + this._treeBox.rowCountChanged(idx + 1, item.children.length); + this._treeBox.invalidateRow(idx); + this._treeBox.endUpdateBatch(); + }.bind(this); + + if (!item.children) { + let fromCacheId = item.level > 0 ? this._panelCacheId : + this._rootCacheId; + this._remoteObjectProvider(fromCacheId, item.objectId, + this._panelCacheId, callback); + } + else { + levelUpdate = false; + callback({object: item.children}); + } } - this._treeBox.invalidateRow(idx); - this._treeBox.endUpdateBatch(); }, getImageSrc: function(idx, column) { }, @@ -424,7 +301,21 @@ PropertyTreeView.prototype = { setCellValue: function(row, col, value) { }, setCellText: function(row, col, value) { }, drop: function(index, orientation, dataTransfer) { }, - canDrop: function(index, orientation, dataTransfer) { return false; } + canDrop: function(index, orientation, dataTransfer) { return false; }, + + _cleanup: function PTV__cleanup() + { + if (this._rows.length) { + // Reset the existing _rows children to the initial state. + this._updateRemoteObject(this._rows, 0); + this._rows = []; + } + + delete this._objectCache; + delete this._rootCacheId; + delete this._panelCacheId; + delete this._remoteObjectProvider; + }, }; /////////////////////////////////////////////////////////////////////////// @@ -477,21 +368,23 @@ function appendChild(aDocument, aParent, aTag, aAttributes) /** * Creates a new PropertyPanel. * + * @see PropertyTreeView * @param nsIDOMNode aParent * Parent node to append the created panel to. - * @param nsIDOMDocument aDocument - * Document to create the new nodes on. * @param string aTitle * Title for the panel. * @param string aObject - * Object to display in the tree. + * Object to display in the tree. For details about this object please + * see the PropertyTreeView constructor in this file. * @param array of objects aButtons * Array with buttons to display at the bottom of the panel. */ -function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons) +function PropertyPanel(aParent, aTitle, aObject, aButtons) { + let document = aParent.ownerDocument; + // Create the underlying panel - this.panel = createElement(aDocument, "panel", { + this.panel = createElement(document, "panel", { label: aTitle, titlebar: "normal", noautofocus: "true", @@ -500,13 +393,13 @@ function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons) }); // Create the tree. - let tree = this.tree = createElement(aDocument, "tree", { + let tree = this.tree = createElement(document, "tree", { flex: 1, hidecolumnpicker: "true" }); - let treecols = aDocument.createElement("treecols"); - appendChild(aDocument, treecols, "treecol", { + let treecols = document.createElement("treecols"); + appendChild(document, treecols, "treecol", { primary: "true", flex: 1, hideheader: "true", @@ -514,18 +407,18 @@ function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons) }); tree.appendChild(treecols); - tree.appendChild(aDocument.createElement("treechildren")); + tree.appendChild(document.createElement("treechildren")); this.panel.appendChild(tree); // Create the footer. - let footer = createElement(aDocument, "hbox", { align: "end" }); - appendChild(aDocument, footer, "spacer", { flex: 1 }); + let footer = createElement(document, "hbox", { align: "end" }); + appendChild(document, footer, "spacer", { flex: 1 }); // The footer can have butttons. let self = this; if (aButtons) { aButtons.forEach(function(button) { - let buttonNode = appendChild(aDocument, footer, "button", { + let buttonNode = appendChild(document, footer, "button", { label: button.label, accesskey: button.accesskey || "", class: button.class || "", @@ -534,7 +427,7 @@ function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons) }); } - appendChild(aDocument, footer, "resizer", { dir: "bottomend" }); + appendChild(document, footer, "resizer", { dir: "bottomend" }); this.panel.appendChild(footer); aParent.appendChild(this.panel); @@ -559,20 +452,15 @@ function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons) } /** - * Destroy the PropertyPanel. This closes the poped up panel and removes - * it from the browser DOM. - * - * @returns void + * Destroy the PropertyPanel. This closes the panel and removes it from the + * browser DOM. */ PropertyPanel.prototype.destroy = function PP_destroy() { + this.treeView.data = null; this.panel.parentNode.removeChild(this.panel); this.treeView = null; this.panel = null; this.tree = null; - - if (this.linkNode) { - this.linkNode._panelOpen = false; - this.linkNode = null; - } } + diff --git a/browser/devtools/webconsole/PropertyPanelAsync.jsm b/browser/devtools/webconsole/PropertyPanelAsync.jsm deleted file mode 100644 index 3164ba448b98..000000000000 --- a/browser/devtools/webconsole/PropertyPanelAsync.jsm +++ /dev/null @@ -1,466 +0,0 @@ -/* -*- Mode: js2; js2-basic-offset: 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 = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () { - let obj = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", obj); - return obj.WebConsoleUtils; -}); - -var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; - - -/////////////////////////////////////////////////////////////////////////// -//// PropertyTreeView. - -/** - * This is an implementation of the nsITreeView interface. For comments on the - * interface properties, see the documentation: - * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsITreeView - */ -var PropertyTreeView = function() { - this._rows = []; - this._objectCache = {}; -}; - -PropertyTreeView.prototype = { - /** - * Stores the visible rows of the tree. - * @private - */ - _rows: null, - - /** - * Stores the nsITreeBoxObject for this tree. - * @private - */ - _treeBox: null, - - /** - * Stores cached information about local objects being inspected. - * @private - */ - _objectCache: null, - - /** - * Use this setter to update the content of the tree. - * - * @param object aObject - * An object that holds information about the object you want to - * display in the property panel. Object properties: - * - object: - * This is the raw object you want to display. You can only provide - * this object if you want the property panel to work in sync mode. - * - remoteObject: - * An array that holds information on the remote object being - * inspected. Each element in this array describes each property in the - * remote object. See WebConsoleUtils.namesAndValuesOf() for details. - * - rootCacheId: - * The cache ID where the objects referenced in remoteObject are found. - * - panelCacheId: - * The cache ID where any object retrieved by this property panel - * instance should be stored into. - * - remoteObjectProvider: - * A function that is invoked when a new object is needed. This is - * called when the user tries to expand an inspectable property. The - * callback must take four arguments: - * - fromCacheId: - * Tells from where to retrieve the object the user picked (from - * which cache ID). - * - objectId: - * The object ID the user wants. - * - panelCacheId: - * Tells in which cache ID to store the objects referenced by - * objectId so they can be retrieved later. - * - callback: - * The callback function to be invoked when the remote object is - * received. This function takes one argument: the raw message - * received from the Web Console content script. - */ - set data(aData) { - let oldLen = this._rows.length; - - this._cleanup(); - - if (!aData) { - return; - } - - if (aData.remoteObject) { - this._rootCacheId = aData.rootCacheId; - this._panelCacheId = aData.panelCacheId; - this._remoteObjectProvider = aData.remoteObjectProvider; - this._rows = [].concat(aData.remoteObject); - this._updateRemoteObject(this._rows, 0); - } - else if (aData.object) { - this._rows = this._inspectObject(aData.object); - } - else { - throw new Error("First argument must have a .remoteObject or " + - "an .object property!"); - } - - if (this._treeBox) { - this._treeBox.beginUpdateBatch(); - if (oldLen) { - this._treeBox.rowCountChanged(0, -oldLen); - } - this._treeBox.rowCountChanged(0, this._rows.length); - this._treeBox.endUpdateBatch(); - } - }, - - /** - * Update a remote object so it can be used with the tree view. This method - * adds properties to each array element. - * - * @private - * @param array aObject - * The remote object you want prepared for use with the tree view. - * @param number aLevel - * The level you want to give to each property in the remote object. - */ - _updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel) - { - aObject.forEach(function(aElement) { - aElement.level = aLevel; - aElement.isOpened = false; - aElement.children = null; - }); - }, - - /** - * Inspect a local object. - * - * @private - * @param object aObject - * The object you want to inspect. - */ - _inspectObject: function PTV__inspectObject(aObject) - { - this._objectCache = {}; - this._remoteObjectProvider = this._localObjectProvider.bind(this); - let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache); - this._updateRemoteObject(children, 0); - return children; - }, - - /** - * An object provider for when the user inspects local objects (not remote - * ones). - * - * @private - * @param string aFromCacheId - * The cache ID from where to retrieve the desired object. - * @param string aObjectId - * The ID of the object you want. - * @param string aDestCacheId - * The ID of the cache where to store any objects referenced by the - * desired object. - * @param function aCallback - * The function you want to receive the object. - */ - _localObjectProvider: - function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId, - aCallback) - { - let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId], - this._objectCache); - aCallback({cacheId: aFromCacheId, - objectId: aObjectId, - object: object, - childrenCacheId: aDestCacheId || aFromCacheId, - }); - }, - - /** nsITreeView interface implementation **/ - - selection: null, - - get rowCount() { return this._rows.length; }, - setTree: function(treeBox) { this._treeBox = treeBox; }, - getCellText: function(idx, column) { - let row = this._rows[idx]; - return row.name + ": " + row.value; - }, - getLevel: function(idx) { - return this._rows[idx].level; - }, - isContainer: function(idx) { - return !!this._rows[idx].inspectable; - }, - isContainerOpen: function(idx) { - return this._rows[idx].isOpened; - }, - isContainerEmpty: function(idx) { return false; }, - isSeparator: function(idx) { return false; }, - isSorted: function() { return false; }, - isEditable: function(idx, column) { return false; }, - isSelectable: function(row, col) { return true; }, - - getParentIndex: function(idx) - { - if (this.getLevel(idx) == 0) { - return -1; - } - for (var t = idx - 1; t >= 0; t--) { - if (this.isContainer(t)) { - return t; - } - } - return -1; - }, - - hasNextSibling: function(idx, after) - { - var thisLevel = this.getLevel(idx); - return this._rows.slice(after + 1).some(function (r) r.level == thisLevel); - }, - - toggleOpenState: function(idx) - { - let item = this._rows[idx]; - if (!item.inspectable) { - return; - } - - if (item.isOpened) { - this._treeBox.beginUpdateBatch(); - item.isOpened = false; - - var thisLevel = item.level; - var t = idx + 1, deleteCount = 0; - while (t < this._rows.length && this.getLevel(t++) > thisLevel) { - deleteCount++; - } - - if (deleteCount) { - this._rows.splice(idx + 1, deleteCount); - this._treeBox.rowCountChanged(idx + 1, -deleteCount); - } - this._treeBox.invalidateRow(idx); - this._treeBox.endUpdateBatch(); - } - else { - let levelUpdate = true; - let callback = function _onRemoteResponse(aResponse) { - this._treeBox.beginUpdateBatch(); - item.isOpened = true; - - if (levelUpdate) { - this._updateRemoteObject(aResponse.object, item.level + 1); - item.children = aResponse.object; - } - - this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children)); - - this._treeBox.rowCountChanged(idx + 1, item.children.length); - this._treeBox.invalidateRow(idx); - this._treeBox.endUpdateBatch(); - }.bind(this); - - if (!item.children) { - let fromCacheId = item.level > 0 ? this._panelCacheId : - this._rootCacheId; - this._remoteObjectProvider(fromCacheId, item.objectId, - this._panelCacheId, callback); - } - else { - levelUpdate = false; - callback({object: item.children}); - } - } - }, - - getImageSrc: function(idx, column) { }, - getProgressMode : function(idx,column) { }, - getCellValue: function(idx, column) { }, - cycleHeader: function(col, elem) { }, - selectionChanged: function() { }, - cycleCell: function(idx, column) { }, - performAction: function(action) { }, - performActionOnCell: function(action, index, column) { }, - performActionOnRow: function(action, row) { }, - getRowProperties: function(idx, column, prop) { }, - getCellProperties: function(idx, column, prop) { }, - getColumnProperties: function(column, element, prop) { }, - - setCellValue: function(row, col, value) { }, - setCellText: function(row, col, value) { }, - drop: function(index, orientation, dataTransfer) { }, - canDrop: function(index, orientation, dataTransfer) { return false; }, - - _cleanup: function PTV__cleanup() - { - if (this._rows.length) { - // Reset the existing _rows children to the initial state. - this._updateRemoteObject(this._rows, 0); - this._rows = []; - } - - delete this._objectCache; - delete this._rootCacheId; - delete this._panelCacheId; - delete this._remoteObjectProvider; - }, -}; - -/////////////////////////////////////////////////////////////////////////// -//// Helper for creating the panel. - -/** - * 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); - for (var attr in aAttributes) { - node.setAttribute(attr, aAttributes[attr]); - } - return node; -} - -/** - * Creates a new DOMNode and appends it to aParent. - * - * @param nsIDOMDocument aDocument - * Document to create the new DOMNode. - * @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 appendChild(aDocument, aParent, aTag, aAttributes) -{ - let node = createElement(aDocument, aTag, aAttributes); - aParent.appendChild(node); - return node; -} - -/////////////////////////////////////////////////////////////////////////// -//// PropertyPanel - -/** - * Creates a new PropertyPanel. - * - * @see PropertyTreeView - * @param nsIDOMNode aParent - * Parent node to append the created panel to. - * @param string aTitle - * Title for the panel. - * @param string aObject - * Object to display in the tree. For details about this object please - * see the PropertyTreeView.data property in this file. - * @param array of objects aButtons - * Array with buttons to display at the bottom of the panel. - */ -function PropertyPanel(aParent, aTitle, aObject, aButtons) -{ - let document = aParent.ownerDocument; - - // Create the underlying panel - this.panel = createElement(document, "panel", { - label: aTitle, - titlebar: "normal", - noautofocus: "true", - noautohide: "true", - close: "true", - }); - - // Create the tree. - let tree = this.tree = createElement(document, "tree", { - flex: 1, - hidecolumnpicker: "true" - }); - - let treecols = document.createElement("treecols"); - appendChild(document, treecols, "treecol", { - primary: "true", - flex: 1, - hideheader: "true", - ignoreincolumnpicker: "true" - }); - tree.appendChild(treecols); - - tree.appendChild(document.createElement("treechildren")); - this.panel.appendChild(tree); - - // Create the footer. - let footer = createElement(document, "hbox", { align: "end" }); - appendChild(document, footer, "spacer", { flex: 1 }); - - // The footer can have butttons. - let self = this; - if (aButtons) { - aButtons.forEach(function(button) { - let buttonNode = appendChild(document, footer, "button", { - label: button.label, - accesskey: button.accesskey || "", - class: button.class || "", - }); - buttonNode.addEventListener("command", button.oncommand, false); - }); - } - - appendChild(document, footer, "resizer", { dir: "bottomend" }); - this.panel.appendChild(footer); - - aParent.appendChild(this.panel); - - // Create the treeView object. - this.treeView = new PropertyTreeView(); - this.treeView.data = aObject; - - // Set the treeView object on the tree view. This has to be done *after* the - // panel is shown. This is because the tree binding must be attached first. - this.panel.addEventListener("popupshown", function onPopupShow() - { - self.panel.removeEventListener("popupshown", onPopupShow, false); - self.tree.view = self.treeView; - }, false); - - this.panel.addEventListener("popuphidden", function onPopupHide() - { - self.panel.removeEventListener("popuphidden", onPopupHide, false); - self.destroy(); - }, false); -} - -/** - * Destroy the PropertyPanel. This closes the panel and removes it from the - * browser DOM. - */ -PropertyPanel.prototype.destroy = function PP_destroy() -{ - this.treeView.data = null; - this.panel.parentNode.removeChild(this.panel); - this.treeView = null; - this.panel = null; - this.tree = null; -} - diff --git a/browser/devtools/webconsole/WebConsoleUtils.jsm b/browser/devtools/webconsole/WebConsoleUtils.jsm index 007b3c813980..21979cf89c72 100644 --- a/browser/devtools/webconsole/WebConsoleUtils.jsm +++ b/browser/devtools/webconsole/WebConsoleUtils.jsm @@ -6,14 +6,14 @@ "use strict"; -let Cc = Components.classes; -let Ci = Components.interfaces; -let Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["WebConsoleUtils"]; +var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider"]; const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; @@ -403,7 +403,7 @@ var WebConsoleUtils = { let m = /^\[object (\S+)\]/.exec(presentable); try { - if (type == "object" && typeof aObject.next == "function" && + if (typeof aObject == "object" && typeof aObject.next == "function" && m && m[1] == "Generator") { return { type: TYPES.GENERATOR, @@ -419,7 +419,8 @@ var WebConsoleUtils = { }; } - if (type == "object" && typeof aObject.__iterator__ == "function") { + if (typeof aObject == "object" && + typeof aObject.__iterator__ == "function") { return { type: TYPES.ITERATOR, display: "Iterator" @@ -514,10 +515,11 @@ var WebConsoleUtils = { let value, presentable; let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; + let deprecated = ["width", "height", "inputEncoding"]; for (let propName in aObject) { - // See bug 632275: skip deprecated width and height properties. - if (isDOMDocument && (propName == "width" || propName == "height")) { + // See bug 632275: skip deprecated properties. + if (isDOMDocument && deprecated.indexOf(propName) > -1) { continue; } @@ -527,8 +529,13 @@ var WebConsoleUtils = { presentable = {type: TYPES.GETTER, display: "Getter"}; } else { - value = aObject[propName]; - presentable = this.presentableValueFor(value); + try { + value = aObject[propName]; + presentable = this.presentableValueFor(value); + } + catch (ex) { + continue; + } } let pair = {}; @@ -704,3 +711,228 @@ WebConsoleUtils.l10n = { XPCOMUtils.defineLazyGetter(WebConsoleUtils.l10n, "stringBundle", function() { return Services.strings.createBundle(STRINGS_URI); }); + + +////////////////////////////////////////////////////////////////////////// +// JS Completer +////////////////////////////////////////////////////////////////////////// + +var JSPropertyProvider = (function _JSPP(WCU) { +const STATE_NORMAL = 0; +const STATE_QUOTE = 2; +const STATE_DQUOTE = 3; + +const OPEN_BODY = "{[(".split(""); +const CLOSE_BODY = "}])".split(""); +const OPEN_CLOSE_BODY = { + "{": "}", + "[": "]", + "(": ")", +}; + +/** + * Analyses a given string to find the last statement that is interesting for + * later completion. + * + * @param string aStr + * A string to analyse. + * + * @returns object + * If there was an error in the string detected, then a object like + * + * { err: "ErrorMesssage" } + * + * is returned, otherwise a object like + * + * { + * state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE, + * startPos: index of where the last statement begins + * } + */ +function findCompletionBeginning(aStr) +{ + let bodyStack = []; + + let state = STATE_NORMAL; + let start = 0; + let c; + for (let i = 0; i < aStr.length; i++) { + c = aStr[i]; + + switch (state) { + // Normal JS state. + case STATE_NORMAL: + if (c == '"') { + state = STATE_DQUOTE; + } + else if (c == "'") { + state = STATE_QUOTE; + } + else if (c == ";") { + start = i + 1; + } + else if (c == " ") { + start = i + 1; + } + else if (OPEN_BODY.indexOf(c) != -1) { + bodyStack.push({ + token: c, + start: start + }); + start = i + 1; + } + else if (CLOSE_BODY.indexOf(c) != -1) { + var last = bodyStack.pop(); + if (!last || OPEN_CLOSE_BODY[last.token] != c) { + return { + err: "syntax error" + }; + } + if (c == "}") { + start = i + 1; + } + else { + start = last.start; + } + } + break; + + // Double quote state > " < + case STATE_DQUOTE: + if (c == "\\") { + i++; + } + else if (c == "\n") { + return { + err: "unterminated string literal" + }; + } + else if (c == '"') { + state = STATE_NORMAL; + } + break; + + // Single quote state > ' < + case STATE_QUOTE: + if (c == "\\") { + i++; + } + else if (c == "\n") { + return { + err: "unterminated string literal" + }; + } + else if (c == "'") { + state = STATE_NORMAL; + } + break; + } + } + + return { + state: state, + startPos: start + }; +} + +/** + * Provides a list of properties, that are possible matches based on the passed + * scope and inputValue. + * + * @param object aScope + * Scope to use for the completion. + * + * @param string aInputValue + * Value that should be completed. + * + * @returns null or object + * If no completion valued could be computed, null is returned, + * otherwise a object with the following form is returned: + * { + * matches: [ string, string, string ], + * matchProp: Last part of the inputValue that was used to find + * the matches-strings. + * } + */ +function JSPropertyProvider(aScope, aInputValue) +{ + let obj = WCU.unwrap(aScope); + + // Analyse the aInputValue and find the beginning of the last part that + // should be completed. + let beginning = findCompletionBeginning(aInputValue); + + // There was an error analysing the string. + if (beginning.err) { + return null; + } + + // If the current state is not STATE_NORMAL, then we are inside of an string + // which means that no completion is possible. + if (beginning.state != STATE_NORMAL) { + return null; + } + + let completionPart = aInputValue.substring(beginning.startPos); + + // Don't complete on just an empty string. + if (completionPart.trim() == "") { + return null; + } + + let properties = completionPart.split("."); + let matchProp; + if (properties.length > 1) { + matchProp = properties.pop().trimLeft(); + for (let i = 0; i < properties.length; i++) { + let prop = properties[i].trim(); + + // If obj is undefined or null, then there is no chance to run completion + // on it. Exit here. + if (typeof obj === "undefined" || obj === null) { + return null; + } + + // Check if prop is a getter function on obj. Functions can change other + // stuff so we can't execute them to get the next object. Stop here. + if (WCU.isNonNativeGetter(obj, prop)) { + return null; + } + try { + obj = obj[prop]; + } + catch (ex) { + return null; + } + } + } + else { + matchProp = properties[0].trimLeft(); + } + + // If obj is undefined or null, then there is no chance to run + // completion on it. Exit here. + if (typeof obj === "undefined" || obj === null) { + return null; + } + + // Skip Iterators and Generators. + if (WCU.isIteratorOrGenerator(obj)) { + return null; + } + + let matches = []; + for (let prop in obj) { + if (prop.indexOf(matchProp) == 0) { + matches.push(prop); + } + } + + return { + matchProp: matchProp, + matches: matches.sort(), + }; +} + +return JSPropertyProvider; +})(WebConsoleUtils); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js index 9fbfc7890507..9f67ffe120d3 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js @@ -8,12 +8,14 @@ let HUD; function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoaded, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } -function tabLoaded() { - browser.removeEventListener("load", tabLoaded, true); - openConsole(); +function consoleOpened(aHud) { + HUD = aHud; content.wrappedJSObject.foobarBug585991 = { "item0": "value0", @@ -22,16 +24,14 @@ function tabLoaded() { "item3": "value3", }; - let hudId = HUDService.getHudIdByWindow(content); - HUD = HUDService.hudReferences[hudId]; let jsterm = HUD.jsterm; let popup = jsterm.autocompletePopup; let completeNode = jsterm.completeNode; ok(!popup.isOpen, "popup is not open"); - popup._panel.addEventListener("popupshown", function() { - popup._panel.removeEventListener("popupshown", arguments.callee, false); + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); @@ -79,7 +79,7 @@ function autocompletePopupHidden() let completeNode = jsterm.completeNode; let inputNode = jsterm.inputNode; - popup._panel.removeEventListener("popuphidden", arguments.callee, false); + popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false); ok(!popup.isOpen, "popup is not open"); @@ -88,8 +88,8 @@ function autocompletePopupHidden() ok(!completeNode.value, "completeNode is empty"); - popup._panel.addEventListener("popupshown", function() { - popup._panel.removeEventListener("popupshown", arguments.callee, false); + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); @@ -104,8 +104,8 @@ function autocompletePopupHidden() is(popup.selectedItem.label, "item0", "item0 is selected"); is(completeNode.value, prefix + "item0", "completeNode.value holds item0"); - popup._panel.addEventListener("popuphidden", function() { - popup._panel.removeEventListener("popuphidden", arguments.callee, false); + popup._panel.addEventListener("popuphidden", function onHidden() { + popup._panel.removeEventListener("popuphidden", onHidden, false); ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); @@ -135,8 +135,8 @@ function testReturnKey() let completeNode = jsterm.completeNode; let inputNode = jsterm.inputNode; - popup._panel.addEventListener("popupshown", function() { - popup._panel.removeEventListener("popupshown", arguments.callee, false); + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); @@ -157,8 +157,8 @@ function testReturnKey() is(popup.selectedItem.label, "item1", "item1 is selected"); is(completeNode.value, prefix + "item1", "completeNode.value holds item1"); - popup._panel.addEventListener("popuphidden", function() { - popup._panel.removeEventListener("popuphidden", arguments.callee, false); + popup._panel.addEventListener("popuphidden", function onHidden() { + popup._panel.removeEventListener("popuphidden", onHidden, false); ok(!popup.isOpen, "popup is not open after VK_RETURN"); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js index 0ebea719e41d..846b7c4ae67a 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js @@ -13,12 +13,8 @@ let HUD; let outputItem; -function tabLoad1(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - - openConsole(); - - HUD = HUDService.getHudByWindow(content); +function consoleOpened(aHud) { + HUD = aHud; outputNode = HUD.outputNode; @@ -26,11 +22,10 @@ function tabLoad1(aEvent) { // Reload so we get some output in the console. browser.contentWindow.location.reload(); - log(document); } function tabLoad2(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.removeEventListener(aEvent.type, tabLoad2, true); outputItem = outputNode.querySelector(".hud-networkinfo .hud-clickable"); ok(outputItem, "found a network message"); @@ -42,7 +37,7 @@ function tabLoad2(aEvent) { } function networkPanelShown(aEvent) { - document.removeEventListener(aEvent.type, arguments.callee, false); + document.removeEventListener(aEvent.type, networkPanelShown, false); document.addEventListener("popupshown", networkPanelShowFailure, false); @@ -57,13 +52,13 @@ function networkPanelShown(aEvent) { } function networkPanelShowFailure(aEvent) { - document.removeEventListener(aEvent.type, arguments.callee, false); + document.removeEventListener(aEvent.type, networkPanelShowFailure, false); ok(false, "the network panel should not show"); } function networkPanelHidden(aEvent) { - this.removeEventListener(aEvent.type, arguments.callee, false); + this.removeEventListener(aEvent.type, networkPanelHidden, false); // The network panel should not show because this is a mouse event that starts // in a position and ends in another. @@ -92,20 +87,27 @@ function networkPanelHidden(aEvent) { HUD.jsterm.setInputValue("document"); HUD.jsterm.execute(); - outputItem = outputNode.querySelector(".webconsole-msg-output " + - ".hud-clickable"); - ok(outputItem, "found a jsterm output message"); + waitForSuccess({ + name: "jsterm output message", + validatorFn: function() + { + return outputNode.querySelector(".webconsole-msg-output .hud-clickable"); + }, + successFn: function() + { + document.addEventListener("popupshown", propertyPanelShown, false); - document.addEventListener("popupshown", properyPanelShown, false); - - // Send the mousedown and click events such that the property panel opens. - EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); - EventUtils.sendMouseEvent({type: "click"}, outputItem); + // Send the mousedown and click events such that the property panel opens. + EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); + EventUtils.sendMouseEvent({type: "click"}, outputItem); + }, + failureFn: finishTest, + }); }); } -function properyPanelShown(aEvent) { - document.removeEventListener(aEvent.type, arguments.callee, false); +function propertyPanelShown(aEvent) { + document.removeEventListener(aEvent.type, propertyPanelShown, false); document.addEventListener("popupshown", propertyPanelShowFailure, false); @@ -120,13 +122,13 @@ function properyPanelShown(aEvent) { } function propertyPanelShowFailure(aEvent) { - document.removeEventListener(aEvent.type, arguments.callee, false); + document.removeEventListener(aEvent.type, propertyPanelShowFailure, false); ok(false, "the property panel should not show"); } function propertyPanelHidden(aEvent) { - this.removeEventListener(aEvent.type, arguments.callee, false); + this.removeEventListener(aEvent.type, propertyPanelHidden, false); // The property panel should not show because this is a mouse event that // starts in a position and ends in another. @@ -149,13 +151,16 @@ function propertyPanelHidden(aEvent) { executeSoon(function() { document.removeEventListener("popupshown", propertyPanelShowFailure, false); - outputItem = null; - finishTest(); + HUD = outputItem = null; + executeSoon(finishTest); }); } function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoad1, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js index c3e8920d2e6c..736ba44ee07e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js @@ -169,6 +169,18 @@ function testGen() { HUD.jsterm.setInputValue("print(" + inputValue + ")"); HUD.jsterm.execute(); + waitForSuccess({ + name: "jsterm print() output for test #" + cpos, + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output:last-child"); + }, + successFn: subtestNext, + failureFn: testNext, + }); + + yield; + outputItem = HUD.outputNode.querySelector(".webconsole-msg-output:" + "last-child"); ok(outputItem, @@ -178,6 +190,32 @@ function testGen() { // Test jsterm execution output. + HUD.jsterm.clearOutput(); + HUD.jsterm.setInputValue(inputValue); + HUD.jsterm.execute(); + + waitForSuccess({ + name: "jsterm output for test #" + cpos, + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output:last-child"); + }, + successFn: subtestNext, + failureFn: testNext, + }); + + yield; + + outputItem = HUD.outputNode.querySelector(".webconsole-msg-output:" + + "last-child"); + ok(outputItem, "found the jsterm output line for inputValues[" + cpos + "]"); + ok(outputItem.textContent.indexOf(expectedOutput) > -1, + "jsterm output is correct for inputValues[" + cpos + "]"); + + let messageBody = outputItem.querySelector(".webconsole-msg-body"); + ok(messageBody, "we have the message body for inputValues[" + cpos + "]"); + + // Test click on output. let eventHandlerID = eventHandlers.length + 1; let propertyPanelShown = function(aEvent) { @@ -205,19 +243,6 @@ function testGen() { eventHandlers.push(propertyPanelShown); - HUD.jsterm.clearOutput(); - HUD.jsterm.setInputValue(inputValue); - HUD.jsterm.execute(); - - outputItem = HUD.outputNode.querySelector(".webconsole-msg-output:" + - "last-child"); - ok(outputItem, "found the jsterm output line for inputValues[" + cpos + "]"); - ok(outputItem.textContent.indexOf(expectedOutput) > -1, - "jsterm output is correct for inputValues[" + cpos + "]"); - - let messageBody = outputItem.querySelector(".webconsole-msg-body"); - ok(messageBody, "we have the message body for inputValues[" + cpos + "]"); - // Send the mousedown, mouseup and click events to check if the property // panel opens. EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window); @@ -251,7 +276,7 @@ function testEnd() { } } - testDriver = null; + HUD = inputValues = testDriver = null; executeSoon(finishTest); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js index f29f1a5b4ecc..dafdcbde3147 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js @@ -7,12 +7,13 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("load", function() { - browser.removeEventListener("load", arguments.callee, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); - openConsole(); - content.location.reload(); - browser.addEventListener("load", tabLoaded, true); + openConsole(null, function() { + content.location.reload(); + browser.addEventListener("load", tabLoaded, true); + }); }, true); } @@ -30,9 +31,6 @@ function tabLoaded() { let networkLink = networkMessage.querySelector(".webconsole-msg-link"); ok(networkLink, "found network message link"); - let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); - ok(jstermMessage, "found output message"); - let popupset = document.getElementById("mainPopupSet"); ok(popupset, "found #mainPopupSet"); @@ -85,6 +83,18 @@ function tabLoaded() { } }); - EventUtils.synthesizeMouse(networkLink, 2, 2, {}); - EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + waitForSuccess({ + name: "jsterm output message", + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); + EventUtils.synthesizeMouse(networkLink, 2, 2, {}); + EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + }, + failureFn: finishTest, + }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js index 4702a09f6136..8ee7f93ff102 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js @@ -46,9 +46,6 @@ function tabLoaded() { let networkLink = networkMessage.querySelector(".webconsole-msg-link"); ok(networkLink, "found network message link"); - let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); - ok(jstermMessage, "found output message"); - let popupset = document.getElementById("mainPopupSet"); ok(popupset, "found #mainPopupSet"); @@ -112,8 +109,20 @@ function tabLoaded() { }); // Show the network and object inspector panels. - EventUtils.synthesizeMouse(networkLink, 2, 2, {}); - EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + waitForSuccess({ + name: "jsterm output message", + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); + EventUtils.synthesizeMouse(networkLink, 2, 2, {}); + EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + }, + failureFn: finishTest, + }); } function togglePBAndThen(callback) { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js index 13f126f26849..03b362057c1e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js @@ -9,35 +9,60 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html"; -function tabLoad(aEvent) { - browser.removeEventListener(aEvent.type, tabLoad, true); +function test$(HUD) { + HUD.jsterm.clearOutput(); - openConsole(null, function(HUD) { - HUD.jsterm.clearOutput(); + HUD.jsterm.setInputValue("$(document.body)"); + HUD.jsterm.execute(); - HUD.jsterm.setInputValue("$(document.body)"); - HUD.jsterm.execute(); + waitForSuccess({ + name: "jsterm output for $()", + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output:last-child"); + }, + successFn: function() + { + let outputItem = HUD.outputNode. + querySelector(".webconsole-msg-output:last-child"); + ok(outputItem.textContent.indexOf("

") > -1, + "jsterm output is correct for $()"); - let outputItem = HUD.outputNode. - querySelector(".webconsole-msg-output:last-child"); - ok(outputItem.textContent.indexOf("

") > -1, - "jsterm output is correct for $()"); + test$$(HUD); + }, + failureFn: test$$.bind(null, HUD), + }); +} - HUD.jsterm.clearOutput(); +function test$$(HUD) { + HUD.jsterm.clearOutput(); - HUD.jsterm.setInputValue("$$(document)"); - HUD.jsterm.execute(); + HUD.jsterm.setInputValue("$$(document)"); + HUD.jsterm.execute(); - outputItem = HUD.outputNode. - querySelector(".webconsole-msg-output:last-child"); - ok(outputItem.textContent.indexOf("621644") > -1, - "jsterm output is correct for $$()"); + waitForSuccess({ + name: "jsterm output for $$()", + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output:last-child"); + }, + successFn: function() + { + let outputItem = HUD.outputNode. + querySelector(".webconsole-msg-output:last-child"); + ok(outputItem.textContent.indexOf("621644") > -1, + "jsterm output is correct for $$()"); - executeSoon(finishTest); + executeSoon(finishTest); + }, + failureFn: finishTest, }); } function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoad, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, test$); + }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js index 7547ccca6a7d..7440ade65046 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js @@ -5,26 +5,27 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoaded, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } -function tabLoaded() { - browser.removeEventListener("load", tabLoaded, true); - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - let HUD = HUDService.hudReferences[hudId]; +function consoleOpened(HUD) { let jsterm = HUD.jsterm; let doc = content.wrappedJSObject.document; - let panel = jsterm.openPropertyPanel("Test1", doc); + let panel = jsterm.openPropertyPanel({ data: { object: doc }}); - let rows = panel.treeView._rows; + let view = panel.treeView; let find = function(regex) { - return rows.some(function(row) { - return regex.test(row.display); - }); + for (let i = 0; i < view.rowCount; i++) { + if (regex.test(view.getCellText(i))) { + return true; + } + } + return false; }; ok(!find(/^(width|height):/), "no document.width/height"); @@ -33,8 +34,8 @@ function tabLoaded() { let getterValue = doc.foobar._val; - panel = jsterm.openPropertyPanel("Test2", doc.foobar); - rows = panel.treeView._rows; + panel = jsterm.openPropertyPanel({ data: { object: doc.foobar }}); + view = panel.treeView; is(getterValue, doc.foobar._val, "getter did not execute"); is(getterValue+1, doc.foobar.val, "getter executed"); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js index e69a54b683fc..7ba687ea0706 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -7,27 +7,25 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoaded, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } -function tabLoaded() { - browser.removeEventListener("load", tabLoaded, true); - +function consoleOpened(HUD) { let tmp = {}; Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); let WCU = tmp.WebConsoleUtils; + let JSPropertyProvider = tmp.JSPropertyProvider; + tmp = null; - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - let HUD = HUDService.hudReferences[hudId]; let jsterm = HUD.jsterm; - let win = content.wrappedJSObject; // Make sure autocomplete does not walk through iterators and generators. let result = win.gen1.next(); - let completion = jsterm.propertyProvider(win, "gen1."); + let completion = JSPropertyProvider(win, "gen1."); is(completion, null, "no matchees for gen1"); ok(!WCU.isObjectInspectable(win.gen1), "gen1 is not inspectable"); @@ -36,7 +34,7 @@ function tabLoaded() { result = win.gen2.next(); - completion = jsterm.propertyProvider(win, "gen2."); + completion = JSPropertyProvider(win, "gen2."); is(completion, null, "no matchees for gen2"); ok(!WCU.isObjectInspectable(win.gen2), "gen2 is not inspectable"); @@ -48,7 +46,7 @@ function tabLoaded() { is(result[0], "foo", "iter1.next() [0] is correct"); is(result[1], "bar", "iter1.next() [1] is correct"); - completion = jsterm.propertyProvider(win, "iter1."); + completion = JSPropertyProvider(win, "iter1."); is(completion, null, "no matchees for iter1"); ok(!WCU.isObjectInspectable(win.iter1), "iter1 is not inspectable"); @@ -57,27 +55,57 @@ function tabLoaded() { is(result[0], "baz", "iter1.next() [0] is correct"); is(result[1], "baaz", "iter1.next() [1] is correct"); - completion = jsterm.propertyProvider(content, "iter2."); + completion = JSPropertyProvider(content, "iter2."); is(completion, null, "no matchees for iter2"); ok(!WCU.isObjectInspectable(win.iter2), "iter2 is not inspectable"); - completion = jsterm.propertyProvider(win, "window."); + completion = JSPropertyProvider(win, "window."); ok(completion, "matches available for window"); ok(completion.matches.length, "matches available for window (length)"); ok(WCU.isObjectInspectable(win), "window is inspectable"); - let panel = jsterm.openPropertyPanel("Test", win); - ok(panel, "opened the Property Panel"); - let rows = panel.treeView._rows; - ok(rows.length, "Property Panel rows are available"); + jsterm.clearOutput(); + + jsterm.setInputValue("window"); + jsterm.execute(); + + waitForSuccess({ + name: "jsterm window object output", + validatorFn: function() + { + return HUD.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + document.addEventListener("popupshown", function onShown(aEvent) { + document.removeEventListener("popupshown", onShown, false); + executeSoon(testPropertyPanel.bind(null, aEvent.target)); + }, false); + + let node = HUD.outputNode.querySelector(".webconsole-msg-output"); + EventUtils.synthesizeMouse(node, 2, 2, {}); + }, + failureFn: finishTest, + }); +} + +function testPropertyPanel(aPanel) { + let tree = aPanel.querySelector("tree"); + let view = tree.view; + let col = tree.columns[0]; + ok(view.rowCount, "Property Panel rowCount"); let find = function(display, children) { - return rows.some(function(row) { - return row.display == display && - row.children == children; - }); + for (let i = 0; i < view.rowCount; i++) { + if (view.isContainer(i) == children && + view.getCellText(i, col) == display) { + return true; + } + } + + return false; }; ok(find("gen1: Generator", false), @@ -92,13 +120,5 @@ function tabLoaded() { ok(find("iter2: Iterator", false), "iter2 is correctly displayed in the Property Panel"); - /* - * - disabled, see bug 632347, c#9 - * ok(find("parent: Window", true), - * "window.parent is correctly displayed in the Property Panel"); - */ - - panel.destroy(); - - finishTest(); + executeSoon(finishTest); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js index c24183dc3af4..0ede58097988 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_642615_autocomplete.js @@ -8,13 +8,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); -function tabLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - let HUD = HUDService.hudReferences[hudId]; +function consoleOpened(HUD) { let jsterm = HUD.jsterm; let stringToCopy = "foobazbarBug642615"; @@ -24,41 +18,11 @@ function tabLoad(aEvent) { jsterm.setInputValue("doc"); + let completionValue; + // wait for key "u" - jsterm.inputNode.addEventListener("keyup", function() { - jsterm.inputNode.removeEventListener("keyup", arguments.callee, false); - - let completionValue = jsterm.completeNode.value; - ok(completionValue, "we have a completeNode.value"); - - // wait for paste - jsterm.inputNode.addEventListener("input", function() { - jsterm.inputNode.removeEventListener("input", arguments.callee, false); - - ok(!jsterm.completeNode.value, "no completeNode.value after clipboard paste"); - - // wait for undo - jsterm.inputNode.addEventListener("input", function() { - jsterm.inputNode.removeEventListener("input", arguments.callee, false); - - is(jsterm.completeNode.value, completionValue, - "same completeNode.value after undo"); - - // wait for paste (via keyboard event) - jsterm.inputNode.addEventListener("keyup", function() { - jsterm.inputNode.removeEventListener("keyup", arguments.callee, false); - - ok(!jsterm.completeNode.value, - "no completeNode.value after clipboard paste (via keyboard event)"); - - executeSoon(finishTest); - }, false); - - EventUtils.synthesizeKey("v", {accelKey: true}); - }, false); - - goDoCommand("cmd_undo"); - }, false); + function onCompletionValue() { + completionValue = jsterm.completeNode.value; // Arguments: expected, setup, success, failure. waitForClipboard( @@ -66,17 +30,73 @@ function tabLoad(aEvent) { function() { clipboardHelper.copyString(stringToCopy); }, - function() { - updateEditUIVisibility(); - goDoCommand("cmd_paste"); + onClipboardCopy, + finishTest); + } + + function onClipboardCopy() { + updateEditUIVisibility(); + goDoCommand("cmd_paste"); + + waitForSuccess(waitForPaste); + } + + let waitForPaste = { + name: "no completion value after paste", + validatorFn: function() + { + return !jsterm.completeNode.value; + }, + successFn: onClipboardPaste, + failureFn: finishTest, + }; + + function onClipboardPaste() { + goDoCommand("cmd_undo"); + waitForSuccess({ + name: "completion value for 'docu' after undo", + validatorFn: function() + { + return !!jsterm.completeNode.value; }, - finish); - }, false); + successFn: onCompletionValueAfterUndo, + failureFn: finishTest, + }); + } + + function onCompletionValueAfterUndo() { + is(jsterm.completeNode.value, completionValue, + "same completeNode.value after undo"); + + EventUtils.synthesizeKey("v", {accelKey: true}); + waitForSuccess({ + name: "no completion after ctrl-v (paste)", + validatorFn: function() + { + return !jsterm.completeNode.value; + }, + successFn: finishTest, + failureFn: finishTest, + }); + } EventUtils.synthesizeKey("u", {}); + + waitForSuccess({ + name: "completion value for 'docu'", + validatorFn: function() + { + return !!jsterm.completeNode.value; + }, + successFn: onCompletionValue, + failureFn: finishTest, + }); } function test() { addTab(TEST_URI); - browser.addEventListener("load", tabLoad, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js index c65a365b22f6..9c94e534a61d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -6,38 +6,36 @@ // Tests that document.body autocompletes in the web console. -let tempScope = {}; -Cu.import("resource:///modules/PropertyPanel.jsm", tempScope); -let PropertyPanel = tempScope.PropertyPanel; -let PropertyTreeView = tempScope.PropertyTreeView; -let namesAndValuesOf = tempScope.namesAndValuesOf; -let isNonNativeGetter = tempScope.isNonNativeGetter; - function test() { addTab("data:text/html;charset=utf-8,Web Console autocompletion bug in document.body"); - browser.addEventListener("load", onLoad, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); } var gHUD; -function onLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - openConsole(); - let hudId = HUDService.getHudIdByWindow(content); - gHUD = HUDService.hudReferences[hudId]; +function consoleOpened(aHud) { + gHUD = aHud; let jsterm = gHUD.jsterm; let popup = jsterm.autocompletePopup; let completeNode = jsterm.completeNode; + let tmp = {}; + Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); + let WCU = tmp.WebConsoleUtils; + tmp = null; + ok(!popup.isOpen, "popup is not open"); - popup._panel.addEventListener("popupshown", function() { - popup._panel.removeEventListener("popupshown", arguments.callee, false); + popup._panel.addEventListener("popupshown", function onShown() { + popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); - let props = namesAndValuesOf(content.wrappedJSObject.document.body).length; - is(popup.itemCount, props, "popup.itemCount is correct"); + let props = WCU.namesAndValuesOf(content.wrappedJSObject.document.body); + is(popup.itemCount, props.length, "popup.itemCount is correct"); popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false); @@ -55,32 +53,80 @@ function autocompletePopupHidden() let completeNode = jsterm.completeNode; let inputNode = jsterm.inputNode; - popup._panel.removeEventListener("popuphidden", arguments.callee, false); + popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false); ok(!popup.isOpen, "popup is not open"); let inputStr = "document.b"; jsterm.setInputValue(inputStr); EventUtils.synthesizeKey("o", {}); let testStr = inputStr.replace(/./g, " ") + " "; - is(completeNode.value, testStr + "dy", "completeNode is empty"); - jsterm.setInputValue(""); - // Check the property panel as well. It's a bit gross to parse the properties - // out of the treeView cell text, but nsITreeView doesn't give us a good - // structured way to get at the data. :-( - let propPanel = jsterm.openPropertyPanel("Test", content.document); + waitForSuccess({ + name: "autocomplete shows document.body", + validatorFn: function() + { + return completeNode.value == testStr + "dy"; + }, + successFn: testPropertyPanel, + failureFn: finishTest, + }); +} + +function testPropertyPanel() +{ + let jsterm = gHUD.jsterm; + jsterm.clearOutput(); + jsterm.setInputValue("document"); + jsterm.execute(); + + waitForSuccess({ + name: "jsterm document object output", + validatorFn: function() + { + return gHUD.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + document.addEventListener("popupshown", function onShown(aEvent) { + document.removeEventListener("popupshown", onShown, false); + executeSoon(propertyPanelShown.bind(null, aEvent.target)); + }, false); + + let node = gHUD.outputNode.querySelector(".webconsole-msg-output"); + EventUtils.synthesizeMouse(node, 2, 2, {}); + }, + failureFn: finishTest, + }); +} + +function propertyPanelShown(aPanel) +{ + let tree = aPanel.querySelector("tree"); + let view = tree.view; + let col = tree.columns[0]; + ok(view.rowCount, "Property Panel rowCount"); + + let foundBody = false; let propPanelProps = []; - for (let idx = 0; idx < propPanel.treeView.rowCount; ++idx) - propPanelProps.push(propPanel.treeView.getCellText(idx, null).split(':')[0]); + for (let idx = 0; idx < view.rowCount; ++idx) { + let text = view.getCellText(idx, col); + if (text == "body: HTMLBodyElement" || text == "body: Object") + foundBody = true; + propPanelProps.push(text.split(":")[0]); + } + // NB: We pull the properties off the prototype, rather than off object itself, // so that expandos like |constructor|, which the propPanel can't see, are not // included. - for (let prop in Object.getPrototypeOf(content.document)) + for (let prop in Object.getPrototypeOf(content.document)) { + if (prop == "inputEncoding") { + continue; + } ok(propPanelProps.indexOf(prop) != -1, "Property |" + prop + "| should be reflected in propertyPanel"); + } + + ok(foundBody, "found document.body"); - let treeRows = propPanel.treeView._rows; - is (treeRows[30].display, "body: Object", "found document.body"); - propPanel.destroy(); executeSoon(finishTest); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js index cf2db39ae43b..4eeaec88a687 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js @@ -5,11 +5,9 @@ // Tests that the $0 console helper works as intended. -let doc; -let h1; - function createDocument() { + let doc = content.document; let div = doc.createElement("div"); let h1 = doc.createElement("h1"); let p1 = doc.createElement("p"); @@ -44,7 +42,7 @@ function createDocument() function setupHighlighterTests() { - h1 = doc.querySelectorAll("h1")[0]; + let h1 = content.document.querySelector("h1"); ok(h1, "we have the header node"); Services.obs.addObserver(runSelectionTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); @@ -58,6 +56,7 @@ function runSelectionTests() executeSoon(function() { InspectorUI.highlighter.addListener("nodeselected", performTestComparisons); + let h1 = content.document.querySelector("h1"); EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content); }); } @@ -67,6 +66,8 @@ function performTestComparisons() InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons); InspectorUI.stopInspecting(); + + let h1 = content.document.querySelector("h1"); is(InspectorUI.highlighter.node, h1, "node selected"); is(InspectorUI.selection, h1, "selection matches node"); @@ -80,16 +81,44 @@ function performWebConsoleTests(hud) jsterm.clearOutput(); jsterm.execute("$0"); - findLogEntry("[object HTMLHeadingElement"); - jsterm.clearOutput(); - let msg = "foo"; - jsterm.execute("$0.textContent = '" + msg + "'"); - findLogEntry(msg); - is(InspectorUI.selection.textContent, msg, "node successfully updated"); + waitForSuccess({ + name: "$0 output", + validatorFn: function() + { + return outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let node = outputNode.querySelector(".webconsole-msg-output"); + isnot(node.textContent.indexOf("[object HTMLHeadingElement"), -1, + "correct output for $0"); - doc = h1 = null; - executeSoon(finishUp); + jsterm.clearOutput(); + jsterm.execute("$0.textContent = 'bug653531'"); + waitForSuccess(waitForNodeUpdate); + }, + failureFn: finishUp, + }); + + let waitForNodeUpdate = { + name: "$0.textContent update", + validatorFn: function() + { + return outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let node = outputNode.querySelector(".webconsole-msg-output"); + isnot(node.textContent.indexOf("bug653531"), -1, + "correct output for $0.textContent"); + is(InspectorUI.selection.textContent, "bug653531", + "node successfully updated"); + + executeSoon(finishUp); + }, + failureFn: finishUp, + }; } function finishUp() { @@ -103,7 +132,6 @@ function test() gBrowser.selectedTab = gBrowser.addTab(); gBrowser.selectedBrowser.addEventListener("load", function() { gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); - doc = content.document; waitForFocus(createDocument, content); }, true); diff --git a/browser/devtools/webconsole/test/browser_webconsole_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_chrome.js index 6e3bc09b3593..cc6e1277252f 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_chrome.js +++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js @@ -6,11 +6,10 @@ // Tests that code completion works properly. function test() { - addTab(getBrowserURL()); - browser.addEventListener("DOMContentLoaded", function onLoad() { - browser.removeEventListener("DOMContentLoaded", onLoad, true); - openConsole(); - testChrome(HUDService.getHudByWindow(content)); + addTab("about:addons"); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testChrome); }, true); } @@ -28,10 +27,9 @@ function testChrome(hud) { // Test typing 'docu'. input.value = "docu"; input.setSelectionRange(4, 4); - jsterm.complete(jsterm.COMPLETE_HINT_ONLY); - is(jsterm.completeNode.value, " ment", "'docu' completion"); - - gBrowser.removeCurrentTab(); - executeSoon(finishTest); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, function() { + is(jsterm.completeNode.value, " ment", "'docu' completion"); + executeSoon(finishTest); + }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_completion.js b/browser/devtools/webconsole/test/browser_webconsole_completion.js index 2cc3f7a59955..12f514522859 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_completion.js +++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js @@ -5,32 +5,46 @@ // Tests that code completion works properly. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_URI = "data:text/html;charset=utf8,

test code completion"; + +let testDriver; function test() { addTab(TEST_URI); - browser.addEventListener("DOMContentLoaded", testCompletion, false); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, function(hud) { + testDriver = testCompletion(hud); + testDriver.next(); + }); + }, true); } -function testCompletion() { - browser.removeEventListener("DOMContentLoaded", testCompletion, false); +function testNext() { + executeSoon(function() { + testDriver.next(); + }); +} - openConsole(); - - var jsterm = HUDService.getHudByWindow(content).jsterm; - var input = jsterm.inputNode; +function testCompletion(hud) { + let jsterm = hud.jsterm; + let input = jsterm.inputNode; // Test typing 'docu'. input.value = "docu"; input.setSelectionRange(4, 4); - jsterm.complete(jsterm.COMPLETE_HINT_ONLY); - is(input.value, "docu", "'docu' completion"); - is(jsterm.completeNode.value, " ment", "'docu' completion"); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield; + + is(input.value, "docu", "'docu' completion (input.value)"); + is(jsterm.completeNode.value, " ment", "'docu' completion (completeNode)"); // Test typing 'docu' and press tab. input.value = "docu"; input.setSelectionRange(4, 4); - jsterm.complete(jsterm.COMPLETE_FORWARD); + jsterm.complete(jsterm.COMPLETE_FORWARD, testNext); + yield; + is(input.value, "document", "'docu' tab completion"); is(input.selectionStart, 8, "start selection is alright"); is(input.selectionEnd, 8, "end selection is alright"); @@ -39,35 +53,45 @@ function testCompletion() { // Test typing 'document.getElem'. input.value = "document.getElem"; input.setSelectionRange(16, 16); - jsterm.complete(jsterm.COMPLETE_FORWARD); + jsterm.complete(jsterm.COMPLETE_FORWARD, testNext); + yield; + is(input.value, "document.getElem", "'document.getElem' completion"); is(jsterm.completeNode.value, " entById", "'document.getElem' completion"); // Test pressing tab another time. - jsterm.complete(jsterm.COMPLETE_FORWARD); + jsterm.complete(jsterm.COMPLETE_FORWARD, testNext); + yield; + is(input.value, "document.getElem", "'document.getElem' completion"); is(jsterm.completeNode.value, " entsByClassName", "'document.getElem' another tab completion"); // Test pressing shift_tab. - jsterm.complete(jsterm.COMPLETE_BACKWARD); + jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext); + yield; + is(input.value, "document.getElem", "'document.getElem' untab completion"); is(jsterm.completeNode.value, " entById", "'document.getElem' completion"); jsterm.clearOutput(); - jsterm.history.splice(0, jsterm.history.length); // workaround for bug 592552 input.value = "docu"; - jsterm.complete(jsterm.COMPLETE_HINT_ONLY); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield; + is(jsterm.completeNode.value, " ment", "'docu' completion"); jsterm.execute(); is(jsterm.completeNode.value, "", "clear completion on execute()"); // Test multi-line completion works input.value = "console.log('one');\nconsol"; - jsterm.complete(jsterm.COMPLETE_HINT_ONLY); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield; + is(jsterm.completeNode.value, " \n e", "multi-line completion"); - jsterm = input = null; - finishTest(); + testDriver = jsterm = input = null; + executeSoon(finishTest); + yield; } diff --git a/browser/devtools/webconsole/test/browser_webconsole_consoleonpage.js b/browser/devtools/webconsole/test/browser_webconsole_consoleonpage.js index fe0044717d93..c85ae9328b74 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_consoleonpage.js +++ b/browser/devtools/webconsole/test/browser_webconsole_consoleonpage.js @@ -14,17 +14,15 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("load", function() { - browser.removeEventListener("load", arguments.callee, true); - testOpenWebConsole(); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testOpenWebConsole); }, true); } -function testOpenWebConsole() +function testOpenWebConsole(aHud) { - openConsole(); - - hud = HUDService.getHudByWindow(content); + hud = aHud; ok(hud, "WebConsole was opened"); testOwnConsole(); @@ -42,8 +40,8 @@ function testOwnConsole() // overwritten by the WebConsole's console. testConsoleOnPage(console); - // Check that the console object is set on the jsterm object although there + // Check that the console object is set on the HUD object although there // is no console object added to the page. - ok(hud.jsterm.console, "JSTerm console is defined"); + ok(hud.console, "HUD console is defined"); finishTest(); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js index 058becce7031..7613f74269b1 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js +++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js @@ -19,17 +19,27 @@ function testExecutionScope(hud) { let jsterm = hud.jsterm; jsterm.clearOutput(); - jsterm.execute("location;"); + jsterm.execute("window.location;"); - let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node"); - is(nodes.length, 2, "Two children in output"); + waitForSuccess({ + name: "jsterm execution output (two nodes)", + validatorFn: function() + { + return jsterm.outputNode.querySelectorAll(".hud-msg-node").length == 2; + }, + successFn: function() + { + let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node"); - is(/location;/.test(nodes[0].textContent), true, - "'location;' written to output"); + is(/window.location;/.test(nodes[0].textContent), true, + "'window.location;' written to output"); - ok(nodes[0].textContent.indexOf(TEST_URI), - "command was executed in the window scope"); + isnot(nodes[1].textContent.indexOf(TEST_URI), -1, + "command was executed in the window scope"); - executeSoon(finishTest); + executeSoon(finishTest); + }, + failureFn: finishTest, + }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_for_of.js b/browser/devtools/webconsole/test/browser_webconsole_for_of.js index c9678c2641ea..fc925535b72d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_for_of.js +++ b/browser/devtools/webconsole/test/browser_webconsole_for_of.js @@ -7,22 +7,29 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("DOMContentLoaded", testForOf, false); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testForOf); + }, true); } -function testForOf() { - browser.removeEventListener("DOMContentLoaded", testForOf, false); - - openConsole(); - var hud = HUDService.getHudByWindow(content); +function testForOf(hud) { var jsterm = hud.jsterm; jsterm.execute("{ [x.tagName for (x of document.body.childNodes) if (x.nodeType === 1)].join(' '); }"); - let node = hud.outputNode.querySelector(".webconsole-msg-output"); - ok(/H1 DIV H2 P/.test(node.textContent), - "for-of loop should find all top-level nodes"); - - jsterm.clearOutput(); - jsterm.history.splice(0, jsterm.history.length); // workaround for bug 592552 - finishTest(); + waitForSuccess({ + name: "jsterm output displayed", + validatorFn: function() + { + return hud.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let node = hud.outputNode.querySelector(".webconsole-msg-output"); + ok(/H1 DIV H2 P/.test(node.textContent), + "for-of loop should find all top-level nodes"); + finishTest(); + }, + failureFn: finishTest, + }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js b/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js index 7d45b2415a79..68bc0c1b719e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js +++ b/browser/devtools/webconsole/test/browser_webconsole_js_input_and_output_styling.js @@ -10,38 +10,38 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/te function test() { addTab(TEST_URI); - browser.addEventListener("DOMContentLoaded", testJSInputAndOutputStyling, - false); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testJSInputAndOutputStyling); + }, true); } -function testJSInputAndOutputStyling() { - browser.removeEventListener("DOMContentLoaded", - testJSInputAndOutputStyling, false); - - openConsole(); - - let jsterm = HUDService.getHudByWindow(content).jsterm; +function testJSInputAndOutputStyling(hud) { + let jsterm = hud.jsterm; jsterm.clearOutput(); jsterm.execute("2 + 2"); - let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node"); - let jsInputNode = nodes[0]; + let jsInputNode = jsterm.outputNode.querySelector(".hud-msg-node"); isnot(jsInputNode.textContent.indexOf("2 + 2"), -1, "JS input node contains '2 + 2'"); ok(jsInputNode.classList.contains("webconsole-msg-input"), "JS input node is of the CSS class 'webconsole-msg-input'"); - let jsOutputNodes = jsterm.outputNode. - querySelectorAll(".webconsole-msg-output"); - isnot(jsOutputNodes[0].textContent.indexOf("4"), -1, - "JS output node contains '4'"); - ok(jsOutputNodes[0].classList.contains("webconsole-msg-output"), - "JS output node is of the CSS class 'webconsole-msg-output'"); - - jsterm.clearOutput(); - jsterm.history.splice(0, jsterm.history.length); // workaround for bug 592552 - - finishTest(); + waitForSuccess({ + name: "jsterm output is displayed", + validatorFn: function() + { + return jsterm.outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { + let node = jsterm.outputNode.querySelector(".webconsole-msg-output"); + isnot(node.textContent.indexOf("4"), -1, + "JS output node contains '4'"); + finishTest(); + }, + failureFn: finishTest, + }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js index b417f5b53cee..79ec34968643 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js +++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js @@ -5,21 +5,46 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; -let jsterm; +let jsterm, testDriver; function test() { addTab(TEST_URI); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); - openConsole(null, testJSTerm); + openConsole(null, function(hud) { + testDriver = testJSTerm(hud); + testDriver.next(); + }); }, true); } +function nextTest() { + testDriver.next(); +} + function checkResult(msg, desc, lines) { - let labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output"); - is(labels.length, lines, "correct number of results shown for " + desc); - is(labels[lines-1].textContent.trim(), msg, "correct message shown for " + - desc); + waitForSuccess({ + name: "correct number of results shown for " + desc, + validatorFn: function() + { + let nodes = jsterm.outputNode.querySelectorAll(".webconsole-msg-output"); + return nodes.length == lines; + }, + successFn: function() + { + let labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output"); + if (typeof msg == "string") { + is(labels[lines-1].textContent.trim(), msg, + "correct message shown for " + desc); + } + else if (typeof msg == "function") { + ok(msg(labels), "correct message shown for " + desc); + } + + nextTest(); + }, + failureFn: nextTest, + }); } function testJSTerm(hud) @@ -29,34 +54,52 @@ function testJSTerm(hud) jsterm.clearOutput(); jsterm.execute("'id=' + $('header').getAttribute('id')"); checkResult('"id=header"', "$() worked", 1); + yield; jsterm.clearOutput(); jsterm.execute("headerQuery = $$('h1')"); jsterm.execute("'length=' + headerQuery.length"); checkResult('"length=1"', "$$() worked", 2); + yield; jsterm.clearOutput(); jsterm.execute("xpathQuery = $x('.//*', document.body);"); jsterm.execute("'headerFound=' + (xpathQuery[0] == headerQuery[0])"); checkResult('"headerFound=true"', "$x() worked", 2); + yield; // no jsterm.clearOutput() here as we clear the output using the clear() fn. jsterm.execute("clear()"); - let group = jsterm.outputNode.querySelector(".hud-group"); - ok(!group, "clear() worked"); + + waitForSuccess({ + name: "clear() worked", + validatorFn: function() + { + return jsterm.outputNode.childNodes.length == 0; + }, + successFn: nextTest, + failureFn: nextTest, + }); + + yield; jsterm.clearOutput(); jsterm.execute("'keysResult=' + (keys({b:1})[0] == 'b')"); checkResult('"keysResult=true"', "keys() worked", 1); + yield; jsterm.clearOutput(); jsterm.execute("'valuesResult=' + (values({b:1})[0] == 1)"); checkResult('"valuesResult=true"', "values() worked", 1); + yield; jsterm.clearOutput(); + + let tabs = gBrowser.tabs.length; + jsterm.execute("help()"); let output = jsterm.outputNode.querySelector(".webconsole-msg-output"); - ok(!group, "help() worked"); + ok(!output, "help() worked"); jsterm.execute("help"); output = jsterm.outputNode.querySelector(".webconsole-msg-output"); @@ -66,57 +109,84 @@ function testJSTerm(hud) output = jsterm.outputNode.querySelector(".webconsole-msg-output"); ok(!output, "? worked"); + let foundTab = null; + waitForSuccess({ + name: "help tab opened", + validatorFn: function() + { + let newTabOpen = gBrowser.tabs.length == tabs + 1; + if (!newTabOpen) { + return false; + } + + foundTab = gBrowser.tabs[tabs]; + return true; + }, + successFn: function() + { + gBrowser.removeTab(foundTab); + nextTest(); + }, + failureFn: nextTest, + }); + yield; + jsterm.clearOutput(); jsterm.execute("pprint({b:2, a:1})"); - // Doesn't conform to checkResult format - let label = jsterm.outputNode.querySelector(".webconsole-msg-output"); - is(label.textContent.trim(), "a: 1\n b: 2", "pprint() worked"); + checkResult("a: 1\n b: 2", "pprint()", 1); + yield; // check instanceof correctness, bug 599940 jsterm.clearOutput(); jsterm.execute("[] instanceof Array"); checkResult("true", "[] instanceof Array == true", 1); + yield; jsterm.clearOutput(); jsterm.execute("({}) instanceof Object"); checkResult("true", "({}) instanceof Object == true", 1); + yield; // check for occurrences of Object XRayWrapper, bug 604430 jsterm.clearOutput(); jsterm.execute("document"); - let label = jsterm.outputNode.querySelector(".webconsole-msg-output"); - is(label.textContent.trim().search(/\[object XrayWrapper/), -1, - "check for non-existence of [object XrayWrapper "); + checkResult(function(nodes) { + return nodes[0].textContent.search(/\[object xraywrapper/i) == -1; + }, "document - no XrayWrapper", 1); + yield; // check that pprint(window) and keys(window) don't throw, bug 608358 jsterm.clearOutput(); jsterm.execute("pprint(window)"); - let labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output"); - is(labels.length, 1, "one line of output for pprint(window)"); + checkResult(null, "pprint(window)", 1); + yield; jsterm.clearOutput(); jsterm.execute("keys(window)"); - labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output"); - is(labels.length, 1, "one line of output for keys(window)"); + checkResult(null, "keys(window)", 1); + yield; + // bug 614561 jsterm.clearOutput(); jsterm.execute("pprint('hi')"); - // Doesn't conform to checkResult format, bug 614561 - let label = jsterm.outputNode.querySelector(".webconsole-msg-output"); - is(label.textContent.trim(), '0: "h"\n 1: "i"', 'pprint("hi") worked'); + checkResult('0: "h"\n 1: "i"', "pprint('hi')", 1); + yield; // check that pprint(function) shows function source, bug 618344 jsterm.clearOutput(); jsterm.execute("pprint(print)"); - label = jsterm.outputNode.querySelector(".webconsole-msg-output"); - isnot(label.textContent.indexOf("SEVERITY_LOG"), -1, - "pprint(function) shows function source"); + checkResult(function(nodes) { + return nodes[0].textContent.indexOf("aJSTerm.") > -1; + }, "pprint(function) shows source", 1); + yield; // check that an evaluated null produces "null", bug 650780 jsterm.clearOutput(); jsterm.execute("null"); checkResult("null", "null is null", 1); + yield; - jsterm = null; + jsterm = testDriver = null; executeSoon(finishTest); + yield; } diff --git a/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js b/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js index 4cf44ea8d035..fff74534ac6c 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js +++ b/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js @@ -23,17 +23,40 @@ function testNullAndUndefinedOutput(hud) { jsterm.clearOutput(); jsterm.execute("null;"); - let nodes = outputNode.querySelectorAll(".hud-msg-node"); - is(nodes.length, 2, "2 nodes in output"); - ok(nodes[1].textContent.indexOf("null") > -1, "'null' printed to output"); + waitForSuccess({ + name: "null displayed", + validatorFn: function() + { + return outputNode.querySelectorAll(".hud-msg-node").length == 2; + }, + successFn: function() + { + let nodes = outputNode.querySelectorAll(".hud-msg-node"); + isnot(nodes[1].textContent.indexOf("null"), -1, + "'null' printed to output"); - jsterm.clearOutput(); - jsterm.execute("undefined;"); + jsterm.clearOutput(); + jsterm.execute("undefined;"); + waitForSuccess(waitForUndefined); + }, + failureFn: finishTest, + }); - nodes = outputNode.querySelectorAll(".hud-msg-node"); - is(nodes.length, 2, "2 nodes in output"); - ok(nodes[1].textContent.indexOf("undefined") > -1, "'undefined' printed to output"); + let waitForUndefined = { + name: "undefined displayed", + validatorFn: function() + { + return outputNode.querySelectorAll(".hud-msg-node").length == 2; + }, + successFn: function() + { + let nodes = outputNode.querySelectorAll(".hud-msg-node"); + isnot(nodes[1].textContent.indexOf("undefined"), -1, + "'undefined' printed to output"); - executeSoon(finishTest); + finishTest(); + }, + failureFn: finishTest, + }; } diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_order.js b/browser/devtools/webconsole/test/browser_webconsole_output_order.js index 4f3dc44389d9..ef3557e5b8ed 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_output_order.js +++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js @@ -36,8 +36,7 @@ function testOutputOrder(hud) { /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent); let outputSecond = /foo bar/.test(nodes[2].textContent); ok(executedStringFirst && outputSecond, "executed string comes first"); - jsterm.clearOutput(); - jsterm.history.splice(0, jsterm.history.length); // workaround for bug 592552 + finishTest(); }, failureFn: finishTest, diff --git a/browser/devtools/webconsole/test/browser_webconsole_property_panel.js b/browser/devtools/webconsole/test/browser_webconsole_property_panel.js index 5ffc9741ea2d..15df293d505e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_property_panel.js +++ b/browser/devtools/webconsole/test/browser_webconsole_property_panel.js @@ -6,56 +6,63 @@ // Tests the functionality of the "property panel", which allows JavaScript // objects and DOM nodes to be inspected. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_URI = "data:text/html;charset=utf8,

property panel test"; function test() { addTab(TEST_URI); - browser.addEventListener("DOMContentLoaded", testPropertyPanel, false); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testPropertyPanel); + }, true); } -function testPropertyPanel() { - browser.removeEventListener("DOMContentLoaded", testPropertyPanel, false); +function testPropertyPanel(hud) { + let jsterm = hud.jsterm; - openConsole(); - - var jsterm = HUDService.getHudByWindow(content).jsterm; - - let propPanel = jsterm.openPropertyPanel("Test", [ - 1, - /abc/, - null, - undefined, - function test() {}, - {} - ]); + let propPanel = jsterm.openPropertyPanel({ + data: { + object: [ + 1, + /abc/, + null, + undefined, + function test() {}, + {} + ] + } + }); is (propPanel.treeView.rowCount, 6, "six elements shown in propertyPanel"); propPanel.destroy(); - propPanel = jsterm.openPropertyPanel("Test2", { - "0.02": 0, - "0.01": 1, - "02": 2, - "1": 3, - "11": 4, - "1.2": 5, - "1.1": 6, - "foo": 7, - "bar": 8 + propPanel = jsterm.openPropertyPanel({ + data: { + object: { + "0.02": 0, + "0.01": 1, + "02": 2, + "1": 3, + "11": 4, + "1.2": 5, + "1.1": 6, + "foo": 7, + "bar": 8 + } + } }); is (propPanel.treeView.rowCount, 9, "nine elements shown in propertyPanel"); - let treeRows = propPanel.treeView._rows; - is (treeRows[0].display, "0.01: 1", "1. element is okay"); - is (treeRows[1].display, "0.02: 0", "2. element is okay"); - is (treeRows[2].display, "1: 3", "3. element is okay"); - is (treeRows[3].display, "1.1: 6", "4. element is okay"); - is (treeRows[4].display, "1.2: 5", "5. element is okay"); - is (treeRows[5].display, "02: 2", "6. element is okay"); - is (treeRows[6].display, "11: 4", "7. element is okay"); - is (treeRows[7].display, "bar: 8", "8. element is okay"); - is (treeRows[8].display, "foo: 7", "9. element is okay"); + let view = propPanel.treeView; + is (view.getCellText(0), "0.01: 1", "1. element is okay"); + is (view.getCellText(1), "0.02: 0", "2. element is okay"); + is (view.getCellText(2), "1: 3", "3. element is okay"); + is (view.getCellText(3), "1.1: 6", "4. element is okay"); + is (view.getCellText(4), "1.2: 5", "5. element is okay"); + is (view.getCellText(5), "02: 2", "6. element is okay"); + is (view.getCellText(6), "11: 4", "7. element is okay"); + is (view.getCellText(7), "bar: 8", "8. element is okay"); + is (view.getCellText(8), "foo: 7", "9. element is okay"); propPanel.destroy(); - finishTest(); + executeSoon(finishTest); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js index 9fbe3d000261..7a98f0dcd119 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js +++ b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js @@ -6,43 +6,37 @@ // Tests the property provider, which is part of the code completion // infrastructure. -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; +const TEST_URI = "data:text/html;charset=utf8,

test the JS property provider"; function test() { addTab(TEST_URI); - browser.addEventListener("DOMContentLoaded", testPropertyProvider, false); + browser.addEventListener("load", testPropertyProvider, true); } function testPropertyProvider() { - browser.removeEventListener("DOMContentLoaded", testPropertyProvider, - false); + browser.removeEventListener("load", testPropertyProvider, true); - openConsole(); + let tmp = {}; + Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); + let JSPropertyProvider = tmp.JSPropertyProvider; + tmp = null; - var HUD = HUDService.getHudByWindow(content); - var jsterm = HUD.jsterm; - var context = jsterm.sandbox.window; - var completion; - - // Test if the propertyProvider can be accessed from the jsterm object. - ok (jsterm.propertyProvider !== undefined, "JSPropertyProvider is defined"); - - completion = jsterm.propertyProvider(context, "thisIsNotDefined"); + let completion = JSPropertyProvider(content, "thisIsNotDefined"); is (completion.matches.length, 0, "no match for 'thisIsNotDefined"); // This is a case the PropertyProvider can't handle. Should return null. - completion = jsterm.propertyProvider(context, "window[1].acb"); + completion = JSPropertyProvider(content, "window[1].acb"); is (completion, null, "no match for 'window[1].acb"); // A very advanced completion case. var strComplete = 'function a() { }document;document.getElementById(window.locatio'; - completion = jsterm.propertyProvider(context, strComplete); + completion = JSPropertyProvider(content, strComplete); ok(completion.matches.length == 2, "two matches found"); ok(completion.matchProp == "locatio", "matching part is 'test'"); ok(completion.matches[0] == "location", "the first match is 'location'"); ok(completion.matches[1] == "locationbar", "the second match is 'locationbar'"); - context = completion = null; + finishTest(); } diff --git a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties index b0a59d804b2c..f33cb7fc7f5b 100644 --- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -191,3 +191,9 @@ timeEnd=%1$S: %2$Sms Autocomplete.blank= <- no result maxTimersExceeded=The maximum allowed number of timers in this page was exceeded. + +# LOCALIZATION NOTE (JSTerm.updateNotInspectable): +# This string is used when the user inspects an evaluation result in the Web +# Console and tries the Update button, but the new result no longer returns an +# object that can be inspected. +JSTerm.updateNotInspectable=After your input has been re-evaluated the result is no longer inspectable.