From 3490516164a32601c55888985998e6b9059dcf8b Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 27 Jan 2012 18:24:57 +0000 Subject: [PATCH] Bug 693269 - Ensure jstermhelpers work in JS mode in GCLI; r=dcamp,msucan --- browser/devtools/webconsole/HUDService.jsm | 115 ++++++++++++++--- browser/devtools/webconsole/gcli.jsm | 98 +++++++------- browser/devtools/webconsole/test/Makefile.in | 1 + .../webconsole/test/browser_gcli_commands.js | 2 - .../webconsole/test/browser_gcli_helpers.js | 122 ++++++++++++++++++ .../browser/devtools/webconsole.properties | 5 + 6 files changed, 281 insertions(+), 62 deletions(-) create mode 100644 browser/devtools/webconsole/test/browser_gcli_helpers.js diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index adc47c554d6a..d12d4353bbc6 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -4708,7 +4708,7 @@ function JSTermHelper(aJSTerm) aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode) { aJSTerm.helperEvaluated = true; - let doc = aJSTerm.parentNode.ownerDocument; + let doc = aJSTerm.inputNode.ownerDocument; let win = doc.defaultView; let panel = createElement(doc, "panel", { label: "CSS Rules", @@ -4822,6 +4822,7 @@ function JSTerm(aContext, aParentNode, aMixin, aConsole) this.parentNode = aParentNode; this.mixins = aMixin; this.console = aConsole; + this.document = aParentNode.ownerDocument this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout; @@ -5027,7 +5028,7 @@ JSTerm.prototype = { }); } - let doc = self.parentNode.ownerDocument; + let doc = self.document; let parent = doc.getElementById("mainPopupSet"); let title = (aEvalString ? HUDService.getFormatStr("jsPropertyInspectTitle", [aEvalString]) @@ -5056,7 +5057,7 @@ JSTerm.prototype = { */ writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject, aOutputString) { - let node = ConsoleUtils.createMessageNode(this.parentNode.ownerDocument, + let node = ConsoleUtils.createMessageNode(this.document, CATEGORY_OUTPUT, SEVERITY_LOG, aOutputString, @@ -5105,7 +5106,7 @@ JSTerm.prototype = { */ writeOutput: function JST_writeOutput(aOutputMessage, aCategory, aSeverity) { - let node = ConsoleUtils.createMessageNode(this.parentNode.ownerDocument, + let node = ConsoleUtils.createMessageNode(this.document, aCategory, aSeverity, aOutputMessage, this.hudId); @@ -6828,6 +6829,7 @@ function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConso this.document = aDocument; this.console = aConsole; this.hintNode = aHintNode; + this._window = this.context.get().QueryInterface(Ci.nsIDOMWindow); this.createUI(); this.createSandbox(); @@ -6924,6 +6926,7 @@ GcliTerm.prototype = { delete this.document; delete this.console; delete this.hintNode; + delete this._window; delete this.sandbox; delete this.element @@ -6983,14 +6986,59 @@ GcliTerm.prototype = { return; } - this.writeOutput(aEvent.output.typed, { category: CATEGORY_INPUT }); + this.writeOutput(aEvent.output.typed, CATEGORY_INPUT); - if (aEvent.output.output == null) { + // This is an experiment to see how much people yell when we stop reporting + // undefined replies. + if (aEvent.output.output === undefined) { return; } let output = aEvent.output.output; - if (aEvent.output.command.returnType == "html" && typeof output == "string") { + let declaredType = aEvent.output.command.returnType || ""; + + if (declaredType == "object") { + let actualType = typeof output; + if (output === null) { + output = "null"; + } + else if (actualType == "string") { + output = "\"" + output + "\""; + } + else if (actualType == "object" || actualType == "function") { + let formatOpts = [ nameObject(output) ]; + output = stringBundle.formatStringFromName('gcliterm.instanceLabel', + formatOpts, formatOpts.length); + let linkNode = this.document.createElementNS(HTML_NS, 'html:span'); + linkNode.appendChild(this.document.createTextNode(output)); + linkNode.classList.add("hud-clickable"); + linkNode.setAttribute("aria-haspopup", "true"); + + // Make the object bring up the property panel. + linkNode.addEventListener("mousedown", function(aEv) { + this._startX = aEv.clientX; + this._startY = aEv.clientY; + }.bind(this), false); + + linkNode.addEventListener("click", function(aEv) { + if (aEv.detail != 1 || aEv.button != 0 || + (this._startX != aEv.clientX && this._startY != aEv.clientY)) { + return; + } + + if (!this._panelOpen) { + let propPanel = this.openPropertyPanel(aEvent.output.typed, aEvent.output.output, this); + propPanel.panel.setAttribute("hudId", this.hudId); + this._panelOpen = true; + } + }.bind(this), false); + + output = linkNode; + } + // else if (actualType == number/boolean/undefined) do nothing + } + + if (declaredType == "html" && typeof output == "string") { output = this.document.createRange().createContextualFragment( '
' + output + '
'); @@ -7030,14 +7078,14 @@ GcliTerm.prototype = { */ createSandbox: function Gcli_createSandbox() { - let win = this.context.get().QueryInterface(Ci.nsIDOMWindow); - // create a JS Sandbox out of this.context - this.sandbox = new Cu.Sandbox(win, { - sandboxPrototype: win, + this.sandbox = new Cu.Sandbox(this._window, { + sandboxPrototype: this._window, wantXrays: false }); this.sandbox.console = this.console; + + JSTermHelper(this); }, /** @@ -7049,7 +7097,31 @@ GcliTerm.prototype = { */ evalInSandbox: function Gcli_evalInSandbox(aString) { - return Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1); + let window = unwrap(this.sandbox.window); + let temp$ = null; + let temp$$ = null; + + // We prefer to execute the page-provided implementations for the $() and + // $$() functions. + if (typeof window.$ == "function") { + temp$ = this.sandbox.$; + delete this.sandbox.$; + } + if (typeof window.$$ == "function") { + temp$$ = this.sandbox.$$; + delete this.sandbox.$$; + } + + let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1); + + if (temp$) { + this.sandbox.$ = temp$; + } + if (temp$$) { + this.sandbox.$$ = temp$$; + } + + return result; }, /** @@ -7063,14 +7135,14 @@ GcliTerm.prototype = { * @param number aSeverity * One of the SEVERITY_ constants. */ - writeOutput: function Gcli_writeOutput(aOutputMessage, aOptions) + writeOutput: function Gcli_writeOutput(aOutputMessage, aCategory, aSeverity, aOptions) { aOptions = aOptions || {}; let node = ConsoleUtils.createMessageNode( this.document, - aOptions.category || CATEGORY_OUTPUT, - aOptions.severity || SEVERITY_LOG, + aCategory || CATEGORY_OUTPUT, + aSeverity || SEVERITY_LOG, aOutputMessage, this.hudId, aOptions.sourceUrl || undefined, @@ -7081,8 +7153,21 @@ GcliTerm.prototype = { }, clearOutput: JSTerm.prototype.clearOutput, + openPropertyPanel: JSTerm.prototype.openPropertyPanel, formatResult: JSTerm.prototype.formatResult, getResultType: JSTerm.prototype.getResultType, formatString: JSTerm.prototype.formatString, }; + +/** + * A fancy version of toString() + */ +function nameObject(aObj) { + if (aObj.constructor && aObj.constructor.name) { + return aObj.constructor.name; + } + // If that fails, use Objects toString which sometimes gives something + // better than 'Object', and at least defaults to Object if nothing better + return Object.prototype.toString.call(aObj).slice(8, -1); +} diff --git a/browser/devtools/webconsole/gcli.jsm b/browser/devtools/webconsole/gcli.jsm index 799543ba6698..b298ae6d6c55 100644 --- a/browser/devtools/webconsole/gcli.jsm +++ b/browser/devtools/webconsole/gcli.jsm @@ -200,6 +200,11 @@ var console = {}; * The constructor name */ function getCtorName(aObj) { + if (aObj.constructor && aObj.constructor.name) { + return aObj.constructor.name; + } + // If that fails, use Objects toString which sometimes gives something + // better than 'Object', and at least defaults to Object if nothing better return Object.prototype.toString.call(aObj).slice(8, -1); } @@ -870,6 +875,10 @@ function Command(commandSpec) { // parameter groups. var usingGroups = false; + if (this.returnType == null) { + this.returnType = 'string'; + } + // In theory this could easily be made recursive, so param groups could // contain nested param groups. Current thinking is that the added // complexity for the UI probably isn't worth it, so this implementation @@ -3020,6 +3029,15 @@ JavascriptType.prototype.parse = function(arg) { var typed = arg.text; var scope = globalObject; + // Just accept numbers + if (!isNaN(parseFloat(typed)) && isFinite(typed)) { + return new Conversion(typed, arg); + } + // Just accept constants like true/false/null/etc + if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) { + return new Conversion(typed, arg); + } + // Analyze the input text and find the beginning of the last part that // should be completed. var beginning = this._findCompletionBeginning(typed); @@ -3984,34 +4002,12 @@ var evalCommandSpec = { description: '' } ], - returnType: 'html', + returnType: 'object', description: { key: 'cliEvalJavascript' }, exec: function(args, context) { - // → is right arrow. We use explicit entities to ensure XML validity - var resultPrefix = '{ ' + args.javascript + ' } → '; - try { - var result = customEval(args.javascript); - - if (result === null) { - return resultPrefix + 'null.'; - } - - if (result === undefined) { - return resultPrefix + 'undefined.'; - } - - if (typeof result === 'function') { - //   is   - return resultPrefix + - (result + '').replace(/\n/g, '
').replace(/ /g, ' '); - } - - return resultPrefix + result; - } - catch (ex) { - return resultPrefix + 'Exception: ' + ex.message; - } - } + return customEval(args.javascript); + }, + evalRegexp: /^\s*{\s*/ }; @@ -4195,8 +4191,9 @@ Requisition.prototype._onAssignmentChange = function(ev) { // Refactor? See bug 660765 // Do preceding arguments need to have dummy values applied so we don't // get a hole in the command line? + var i; if (ev.assignment.param.isPositionalAllowed()) { - for (var i = 0; i < ev.assignment.paramIndex; i++) { + for (i = 0; i < ev.assignment.paramIndex; i++) { var assignment = this.getAssignment(i); if (assignment.param.isPositionalAllowed()) { if (assignment.ensureVisibleArgument()) { @@ -4208,7 +4205,7 @@ Requisition.prototype._onAssignmentChange = function(ev) { // Remember where we found the first match var index = MORE_THAN_THE_MOST_ARGS_POSSIBLE; - for (var i = 0; i < this._args.length; i++) { + for (i = 0; i < this._args.length; i++) { if (this._args[i].assignment === ev.assignment) { if (i < index) { index = i; @@ -4224,7 +4221,7 @@ Requisition.prototype._onAssignmentChange = function(ev) { else { // Is there a way to do this that doesn't involve a loop? var newArgs = ev.conversion.arg.getArgs(); - for (var i = 0; i < newArgs.length; i++) { + for (i = 0; i < newArgs.length; i++) { this._args.splice(index + i, 0, newArgs[i]); } } @@ -4267,14 +4264,14 @@ Requisition.prototype.getAssignment = function(nameOrNumber) { nameOrNumber : Object.keys(this._assignments)[nameOrNumber]; return this._assignments[name] || undefined; -}, +}; /** * Where parameter name == assignment names - they are the same */ Requisition.prototype.getParameterNames = function() { return Object.keys(this._assignments); -}, +}; /** * A *shallow* clone of the assignments. @@ -4407,14 +4404,15 @@ Requisition.prototype.createInputArgTrace = function() { } var args = []; + var i; this._args.forEach(function(arg) { - for (var i = 0; i < arg.prefix.length; i++) { + for (i = 0; i < arg.prefix.length; i++) { args.push({ arg: arg, char: arg.prefix[i], part: 'prefix' }); } - for (var i = 0; i < arg.text.length; i++) { + for (i = 0; i < arg.text.length; i++) { args.push({ arg: arg, char: arg.text[i], part: 'text' }); } - for (var i = 0; i < arg.suffix.length; i++) { + for (i = 0; i < arg.suffix.length; i++) { args.push({ arg: arg, char: arg.suffix[i], part: 'suffix' }); } }); @@ -4575,10 +4573,18 @@ Requisition.prototype.exec = function(input) { return false; } + // Display JavaScript input without the initial { or closing } + var typed = this.toString(); + if (evalCommandSpec.evalRegexp.test(typed)) { + typed = typed.replace(evalCommandSpec.evalRegexp, ''); + // Bug 717763: What if the JavaScript naturally ends with a }? + typed = typed.replace(/\s*}\s*$/, ''); + } + var outputObject = { command: command, args: args, - typed: this.toString(), + typed: typed, canonical: this.toCanonicalString(), completed: false, start: new Date() @@ -4586,7 +4592,7 @@ Requisition.prototype.exec = function(input) { this.commandOutputManager.sendCommandOutput(outputObject); - var onComplete = (function(output, error) { + var onComplete = function(output, error) { if (visible) { outputObject.end = new Date(); outputObject.duration = outputObject.end.getTime() - outputObject.start.getTime(); @@ -4595,7 +4601,7 @@ Requisition.prototype.exec = function(input) { outputObject.completed = true; this.commandOutputManager.sendCommandOutput(outputObject); } - }).bind(this); + }.bind(this); try { var context = new ExecutionContext(this); @@ -4614,6 +4620,7 @@ Requisition.prototype.exec = function(input) { } } catch (ex) { + console.error(ex); onComplete(ex, true); } @@ -4768,6 +4775,7 @@ Requisition.prototype._tokenize = function(typed) { while (true) { var c = typed[i]; + var str; switch (mode) { case In.WHITESPACE: if (c === '\'') { @@ -4800,7 +4808,7 @@ Requisition.prototype._tokenize = function(typed) { // There is an edge case of xx'xx which we are assuming to // be a single parameter (and same with ") if (c === ' ') { - var str = unescape2(typed.substring(start, i)); + str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, '')); mode = In.WHITESPACE; start = i; @@ -4810,7 +4818,7 @@ Requisition.prototype._tokenize = function(typed) { case In.SINGLE_Q: if (c === '\'') { - var str = unescape2(typed.substring(start, i)); + str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; @@ -4820,7 +4828,7 @@ Requisition.prototype._tokenize = function(typed) { case In.DOUBLE_Q: if (c === '"') { - var str = unescape2(typed.substring(start, i)); + str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; @@ -4835,7 +4843,7 @@ Requisition.prototype._tokenize = function(typed) { else if (c === '}') { blockDepth--; if (blockDepth === 0) { - var str = unescape2(typed.substring(start, i)); + str = unescape2(typed.substring(start, i)); args.push(new ScriptArgument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; @@ -4864,11 +4872,11 @@ Requisition.prototype._tokenize = function(typed) { } } else if (mode === In.SCRIPT) { - var str = unescape2(typed.substring(start, i + 1)); + str = unescape2(typed.substring(start, i + 1)); args.push(new ScriptArgument(str, prefix, '')); } else { - var str = unescape2(typed.substring(start, i + 1)); + str = unescape2(typed.substring(start, i + 1)); args.push(new Argument(str, prefix, '')); } break; @@ -4901,16 +4909,16 @@ Requisition.prototype._split = function(args) { // Handle the special case of the user typing { javascript(); } // We use the hidden 'eval' command directly rather than shift()ing one of // the parameters, and parse()ing it. + var conversion; if (args[0] instanceof ScriptArgument) { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command - var conversion = new Conversion(evalCommand, new Argument()); + conversion = new Conversion(evalCommand, new Argument()); this.commandAssignment.setConversion(conversion); return; } var argsUsed = 1; - var conversion; while (argsUsed <= args.length) { var arg = (argsUsed === 1) ? diff --git a/browser/devtools/webconsole/test/Makefile.in b/browser/devtools/webconsole/test/Makefile.in index f0a909f64722..a850e13917a9 100644 --- a/browser/devtools/webconsole/test/Makefile.in +++ b/browser/devtools/webconsole/test/Makefile.in @@ -144,6 +144,7 @@ _BROWSER_TEST_FILES = \ browser_webconsole_bug_664131_console_group.js \ browser_webconsole_bug_704295.js \ browser_gcli_commands.js \ + browser_gcli_helpers.js \ browser_gcli_inspect.js \ browser_gcli_integrate.js \ browser_gcli_require.js \ diff --git a/browser/devtools/webconsole/test/browser_gcli_commands.js b/browser/devtools/webconsole/test/browser_gcli_commands.js index 35da14917e71..56e067a65b7f 100644 --- a/browser/devtools/webconsole/test/browser_gcli_commands.js +++ b/browser/devtools/webconsole/test/browser_gcli_commands.js @@ -5,8 +5,6 @@ // - https://github.com/mozilla/gcli/blob/master/docs/index.md // - https://wiki.mozilla.org/DevTools/Features/GCLI -// Tests that the inspect command works as it should - Components.utils.import("resource:///modules/gcli.jsm"); let hud; diff --git a/browser/devtools/webconsole/test/browser_gcli_helpers.js b/browser/devtools/webconsole/test/browser_gcli_helpers.js new file mode 100644 index 000000000000..0a397bdd4610 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_gcli_helpers.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// For more information on GCLI see: +// - https://github.com/mozilla/gcli/blob/master/docs/index.md +// - https://wiki.mozilla.org/DevTools/Features/GCLI + +Components.utils.import("resource:///modules/gcli.jsm"); + +let hud; +let gcliterm; + +registerCleanupFunction(function() { + gcliterm = undefined; + hud = undefined; + Services.prefs.clearUserPref("devtools.gcli.enable"); +}); + +function test() { + Services.prefs.setBoolPref("devtools.gcli.enable", true); + addTab("http://example.com/browser/browser/devtools/webconsole/test//test-console.html"); + browser.addEventListener("DOMContentLoaded", onLoad, false); +} + +function onLoad() { + browser.removeEventListener("DOMContentLoaded", onLoad, false); + + openConsole(); + hud = HUDService.getHudByWindow(content); + gcliterm = hud.gcliterm; + + testHelpers(); + testScripts(); + + closeConsole(); + finishTest(); + + // gcli._internal.console.error("Command Tests Completed"); +} + +function testScripts() { + check("{ 'id=' + $('header').getAttribute('id')", '"id=header"'); + check("{ headerQuery = $$('h1')", "Instance of NodeList"); + check("{ 'length=' + headerQuery.length", '"length=1"'); + + check("{ xpathQuery = $x('.//*', document.body);", 'Instance of Array'); + check("{ 'headerFound=' + (xpathQuery[0] == headerQuery[0])", '"headerFound=true"'); + + check("{ 'keysResult=' + (keys({b:1})[0] == 'b')", '"keysResult=true"'); + check("{ 'valuesResult=' + (values({b:1})[0] == 1)", '"valuesResult=true"'); + + check("{ [] instanceof Array", "true"); + check("{ ({}) instanceof Object", "true"); + check("{ document", "Instance of HTMLDocument"); + check("{ null", "null"); + check("{ undefined", undefined); + + check("{ for (var x=0; x<9; x++) x;", "8"); +} + +function check(command, reply) { + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput(command); + gcliterm.opts.requisition.exec(); + + let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output"); + if (reply === undefined) { + is(labels.length, 0, "results count for: " + command); + } + else { + is(labels.length, 1, "results count for: " + command); + is(labels[0].textContent.trim(), reply, "message for: " + command); + } + + gcliterm.opts.console.inputter.setInput(""); +} + +function testHelpers() { + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput("{ pprint({b:2, a:1})"); + gcliterm.opts.requisition.exec(); + // Doesn't conform to check() format + let label = hud.outputNode.querySelector(".webconsole-msg-output"); + is(label.textContent.trim(), "a: 1\n b: 2", "pprint() worked"); + + // no gcliterm.clearOutput() here as we clear the output using the clear() fn. + gcliterm.opts.console.inputter.setInput("{ clear()"); + gcliterm.opts.requisition.exec(); + ok(!hud.outputNode.querySelector(".hud-group"), "clear() worked"); + + // check that pprint(window) and keys(window) don't throw, bug 608358 + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput("{ pprint(window)"); + gcliterm.opts.requisition.exec(); + let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output"); + is(labels.length, 1, "one line of output for pprint(window)"); + + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput("{ keys(window)"); + gcliterm.opts.requisition.exec(); + labels = hud.outputNode.querySelectorAll(".webconsole-msg-output"); + is(labels.length, 1, "one line of output for keys(window)"); + + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput("{ pprint('hi')"); + gcliterm.opts.requisition.exec(); + // Doesn't conform to check() format, bug 614561 + label = hud.outputNode.querySelector(".webconsole-msg-output"); + is(label.textContent.trim(), '0: "h"\n 1: "i"', 'pprint("hi") worked'); + + // Causes a memory leak. FIXME Bug 717892 + /* + // check that pprint(function) shows function source, bug 618344 + gcliterm.clearOutput(); + gcliterm.opts.console.inputter.setInput("{ pprint(print)"); + gcliterm.opts.requisition.exec(); + label = hud.outputNode.querySelector(".webconsole-msg-output"); + isnot(label.textContent.indexOf("SEVERITY_LOG"), -1, "pprint(function) shows function source"); + */ + + gcliterm.clearOutput(); +} diff --git a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties index d8c0cfbe39a8..c4452f0bef96 100644 --- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -151,6 +151,11 @@ webConsoleWindowTitleAndURL=Web Console - %S # Javascript is being entered, to indicate how to jump into scratchpad mode scratchpad.linkText=Shift+RETURN - Open in Scratchpad +# LOCALIZATION NOTE (gcliterm.instanceLabel): The console displays +# objects using their type (from the constructor function) in this descriptive +# string +gcliterm.instanceLabel=Instance of %S + # LOCALIZATION NOTE (Autocomplete.label): # The autocomplete popup panel label/title. Autocomplete.label=Autocomplete popup