author | Joe Walker <jwalker@mozilla.com> |
Fri, 24 Aug 2012 16:04:45 +0100 | |
changeset 103445 | c80d0e010be310c2770e6668a89ee5cce1e97f3d |
parent 103444 | a275a3c32e714c1bc5e8e886e60b15804f4dd5c5 |
child 103446 | 73b1af99b72db8051f53b8fff859637cc980f740 |
push id | 13991 |
push user | ryanvm@gmail.com |
push date | Sun, 26 Aug 2012 02:29:03 +0000 |
treeherder | mozilla-inbound@c4f20a024113 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | dcamp |
bugs | 773565 |
milestone | 17.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/devtools/commandline/CmdJsb.jsm +++ b/browser/devtools/commandline/CmdJsb.jsm @@ -16,17 +16,16 @@ XPCOMUtils.defineLazyModuleGetter(this, /** * jsb command. */ gcli.addCommand({ name: 'jsb', description: gcli.lookup('jsbDesc'), returnValue:'string', - hidden: true, params: [ { name: 'url', type: 'string', description: gcli.lookup('jsbUrlDesc'), manual: 'The URL of the JS to prettify' }, { @@ -35,104 +34,103 @@ gcli.addCommand({ description: gcli.lookup('jsbIndentSizeDesc'), manual: gcli.lookup('jsbIndentSizeManual'), defaultValue: 2 }, { name: 'indentChar', type: { name: 'selection', - lookup: [{name: "space", value: " "}, {name: "tab", value: "\t"}] + lookup: [ + { name: "space", value: " " }, + { name: "tab", value: "\t" } + ] }, description: gcli.lookup('jsbIndentCharDesc'), manual: gcli.lookup('jsbIndentCharManual'), defaultValue: ' ', }, { name: 'preserveNewlines', type: 'boolean', description: gcli.lookup('jsbPreserveNewlinesDesc'), - manual: gcli.lookup('jsbPreserveNewlinesManual'), - defaultValue: true + manual: gcli.lookup('jsbPreserveNewlinesManual') }, { name: 'preserveMaxNewlines', type: 'number', description: gcli.lookup('jsbPreserveMaxNewlinesDesc'), manual: gcli.lookup('jsbPreserveMaxNewlinesManual'), defaultValue: -1 }, { name: 'jslintHappy', type: 'boolean', description: gcli.lookup('jsbJslintHappyDesc'), - manual: gcli.lookup('jsbJslintHappyManual'), - defaultValue: false + manual: gcli.lookup('jsbJslintHappyManual') }, { name: 'braceStyle', type: { name: 'selection', data: ['collapse', 'expand', 'end-expand', 'expand-strict'] }, description: gcli.lookup('jsbBraceStyleDesc'), manual: gcli.lookup('jsbBraceStyleManual'), defaultValue: "collapse" }, { name: 'spaceBeforeConditional', type: 'boolean', description: gcli.lookup('jsbSpaceBeforeConditionalDesc'), - manual: gcli.lookup('jsbSpaceBeforeConditionalManual'), - defaultValue: true + manual: gcli.lookup('jsbSpaceBeforeConditionalManual') }, { name: 'unescapeStrings', type: 'boolean', description: gcli.lookup('jsbUnescapeStringsDesc'), - manual: gcli.lookup('jsbUnescapeStringsManual'), - defaultValue: false + manual: gcli.lookup('jsbUnescapeStringsManual') } ], exec: function(args, context) { - let opts = { - indent_size: args.indentSize, - indent_char: args.indentChar, - preserve_newlines: args.preserveNewlines, - max_preserve_newlines: args.preserveMaxNewlines == -1 ? - undefined : args.preserveMaxNewlines, - jslint_happy: args.jslintHappy, - brace_style: args.braceStyle, - space_before_conditional: args.spaceBeforeConditional, - unescape_strings: args.unescapeStrings - } + let opts = { + indent_size: args.indentSize, + indent_char: args.indentChar, + preserve_newlines: args.preserveNewlines, + max_preserve_newlines: args.preserveMaxNewlines == -1 ? + undefined : args.preserveMaxNewlines, + jslint_happy: args.jslintHappy, + brace_style: args.braceStyle, + space_before_conditional: args.spaceBeforeConditional, + unescape_strings: args.unescapeStrings + } - let xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest(); - try { - xhr.open("GET", args.url, true); - } catch(e) { - return gcli.lookup('jsbInvalidURL'); - } + try { + xhr.open("GET", args.url, true); + } catch(e) { + return gcli.lookup('jsbInvalidURL'); + } - let promise = context.createPromise(); - - xhr.onreadystatechange = function(aEvt) { - if (xhr.readyState == 4) { - if (xhr.status == 200 || xhr.status == 0) { - let browserDoc = context.environment.chromeDocument; - let browserWindow = browserDoc.defaultView; - let browser = browserWindow.gBrowser; + let promise = context.createPromise(); - browser.selectedTab = browser.addTab("data:text/plain;base64," + - browserWindow.btoa(js_beautify(xhr.responseText, opts))); - promise.resolve(); - } - else { - promise.resolve("Unable to load page to beautify: " + args.url + " " + - xhr.status + " " + xhr.statusText); - } - }; - } - xhr.send(null); - return promise; + xhr.onreadystatechange = function(aEvt) { + if (xhr.readyState == 4) { + if (xhr.status == 200 || xhr.status == 0) { + let browserDoc = context.environment.chromeDocument; + let browserWindow = browserDoc.defaultView; + let browser = browserWindow.gBrowser; + + browser.selectedTab = browser.addTab("data:text/plain;base64," + + browserWindow.btoa(js_beautify(xhr.responseText, opts))); + promise.resolve(); + } + else { + promise.resolve("Unable to load page to beautify: " + args.url + " " + + xhr.status + " " + xhr.statusText); + } + }; + } + xhr.send(null); + return promise; } });
--- a/browser/devtools/commandline/CmdRestart.jsm +++ b/browser/devtools/commandline/CmdRestart.jsm @@ -23,17 +23,16 @@ Cu.import("resource://gre/modules/Servic */ gcli.addCommand({ name: "restart", description: gcli.lookup("restartFirefoxDesc"), params: [ { name: "nocache", type: "boolean", - defaultValue: false, description: gcli.lookup("restartFirefoxNocacheDesc") } ], returnType: "string", exec: function Restart(args, context) { let canceled = Cc["@mozilla.org/supports-PRBool;1"] .createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
--- a/browser/devtools/commandline/CmdScreenshot.jsm +++ b/browser/devtools/commandline/CmdScreenshot.jsm @@ -32,17 +32,16 @@ gcli.addCommand({ type: { name: "number", min: 0 }, defaultValue: 0, description: gcli.lookup("screenshotDelayDesc"), manual: gcli.lookup("screenshotDelayManual") }, { name: "fullpage", type: "boolean", - defaultValue: false, description: gcli.lookup("screenshotFullPageDesc"), manual: gcli.lookup("screenshotFullPageManual") }, { name: "node", type: "node", defaultValue: null, description: gcli.lookup("inspectNodeDesc"),
--- a/browser/devtools/commandline/gcli.jsm +++ b/browser/devtools/commandline/gcli.jsm @@ -800,47 +800,18 @@ Conversion.prototype.toString = function Conversion.prototype.getPredictions = function() { if (typeof this.predictions === 'function') { return this.predictions(); } return this.predictions || []; }; /** - * Accessor for a prediction by index. - * This is useful above <tt>getPredictions()[index]</tt> because it normalizes - * index to be within the bounds of the predictions, which means that the UI - * can maintain an index of which prediction to choose without caring how many - * predictions there are. - * @param index The index of the prediction to choose - */ -Conversion.prototype.getPredictionAt = function(index) { - if (index == null) { - return undefined; - } - - var predictions = this.getPredictions(); - if (predictions.length === 0) { - return undefined; - } - - index = index % predictions.length; - if (index < 0) { - index = predictions.length + index; - } - return predictions[index]; -}; - -/** - * Accessor for a prediction by index. - * This is useful above <tt>getPredictions()[index]</tt> because it normalizes - * index to be within the bounds of the predictions, which means that the UI - * can maintain an index of which prediction to choose without caring how many - * predictions there are. - * @param index The index of the prediction to choose + * Return an index constrained by the available predictions. Basically + * (index % predicitons.length) */ Conversion.prototype.constrainPredictionIndex = function(index) { if (index == null) { return undefined; } var predictions = this.getPredictions(); if (predictions.length === 0) { @@ -1122,17 +1093,16 @@ exports.getType = function(typeSpec) { * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/argument', ['require', 'exports', 'module' ], function(require, exports, module) { -var argument = exports; /** * Thinking out loud here: * Arguments are an area where we could probably refactor things a bit better. * The split process in Requisition creates a set of Arguments, which are then * assigned. The assign process sometimes converts them into subtypes of * Argument. We might consider that what gets assigned is _always_ one of the @@ -1176,34 +1146,62 @@ Argument.prototype.merge = function(foll // Is it possible that this gets called when we're merging arguments // for the single string? return new Argument( this.text + this.suffix + following.prefix + following.text, this.prefix, following.suffix); }; /** - * Returns a new Argument like this one but with the text set to - * <tt>replText</tt> and the end adjusted to fit. - * @param replText Text to replace the old text value - */ -Argument.prototype.beget = function(replText, options) { + * Returns a new Argument like this one but with various items changed. + * @param options Values to use in creating a new Argument. + * Warning: some implementations of beget make additions to the options + * argument. You should be aware of this in the unlikely event that you want to + * reuse 'options' arguments. + * Properties: + * - text: The new text value + * - prefixSpace: Should the prefix be altered to begin with a space? + * - prefixPostSpace: Should the prefix be altered to end with a space? + * - suffixSpace: Should the suffix be altered to end with a space? + * - type: Constructor to use in creating new instances. Default: Argument + */ +Argument.prototype.beget = function(options) { + var text = this.text; var prefix = this.prefix; var suffix = this.suffix; - // We need to add quotes when the replacement string has spaces or is empty - var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ? - '\'' : ''; - - if (options) { - prefix = (options.prefixSpace ? ' ' : '') + quote; - suffix = quote; - } - - return new Argument(replText, prefix, suffix); + if (options.text != null) { + text = options.text; + + // We need to add quotes when the replacement string has spaces or is empty + var needsQuote = text.indexOf(' ') >= 0 || text.length == 0; + if (needsQuote && /['"]/.test(prefix)) { + prefix = prefix + '\''; + suffix = '\'' + suffix; + } + } + + if (options.prefixSpace && prefix.charAt(0) !== ' ') { + prefix = ' ' + prefix; + } + + if (options.prefixPostSpace && prefix.charAt(prefix.length - 1) !== ' ') { + prefix = prefix + ' '; + } + + if (options.suffixSpace && suffix.charAt(suffix.length - 1) !== ' ') { + suffix = suffix + ' '; + } + + if (text === this.text && suffix === this.suffix && prefix === this.prefix) { + return this; + } + + var type = options.type || Argument; + return new type(text, prefix, suffix); }; /** * We need to keep track of which assignment we've been assigned to */ Argument.prototype.assign = function(assignment) { this.assignment = assignment; }; @@ -1277,81 +1275,85 @@ Object.defineProperty(Argument.prototype 'null' : this.assignment.param.name; return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' + ' (a=' + assignStatus + ',' + ' t=' + this.type + ')'; }, enumerable: true }); -argument.Argument = Argument; +exports.Argument = Argument; /** * BlankArgument is a marker that the argument wasn't typed but is there to * fill a slot. Assignments begin with their arg set to a BlankArgument. */ function BlankArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } BlankArgument.prototype = Object.create(Argument.prototype); BlankArgument.prototype.type = 'BlankArgument'; -argument.BlankArgument = BlankArgument; +exports.BlankArgument = BlankArgument; /** * ScriptArgument is a marker that the argument is designed to be Javascript. * It also implements the special rules that spaces after the { or before the * } are part of the pre/suffix rather than the content, and that they are * never 'blank' so they can be used by Requisition._split() and not raise an * ERROR status due to being blank. */ function ScriptArgument(text, prefix, suffix) { this.text = text !== undefined ? text : ''; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; - while (this.text.charAt(0) === ' ') { - this.prefix = this.prefix + ' '; - this.text = this.text.substring(1); - } - - while (this.text.charAt(this.text.length - 1) === ' ') { - this.suffix = ' ' + this.suffix; - this.text = this.text.slice(0, -1); - } + ScriptArgument._moveSpaces(this); } ScriptArgument.prototype = Object.create(Argument.prototype); ScriptArgument.prototype.type = 'ScriptArgument'; /** - * Returns a new Argument like this one but with the text set to - * <tt>replText</tt> and the end adjusted to fit. - * @param replText Text to replace the old text value - */ -ScriptArgument.prototype.beget = function(replText, options) { - var prefix = this.prefix; - var suffix = this.suffix; - - if (options && options.normalize) { - prefix = '{ '; - suffix = ' }'; - } - - return new ScriptArgument(replText, prefix, suffix); -}; - -argument.ScriptArgument = ScriptArgument; + * Private/Dangerous: Alters a ScriptArgument to move the spaces at the start + * or end of the 'text' into the prefix/suffix. With a string, " a " is 3 chars + * long, but with a ScriptArgument, { a } is only one char long. + * Arguments are generally supposed to be immutable, so this method should only + * be called on a ScriptArgument that isn't exposed to the outside world yet. + */ +ScriptArgument._moveSpaces = function(arg) { + while (arg.text.charAt(0) === ' ') { + arg.prefix = arg.prefix + ' '; + arg.text = arg.text.substring(1); + } + + while (arg.text.charAt(arg.text.length - 1) === ' ') { + arg.suffix = ' ' + arg.suffix; + arg.text = arg.text.slice(0, -1); + } +}; + +/** + * As Argument.beget that implements the space rule documented in the ctor. + */ +ScriptArgument.prototype.beget = function(options) { + options.type = ScriptArgument; + var begotten = Argument.prototype.beget.call(this, options); + ScriptArgument._moveSpaces(begotten); + return begotten; +}; + +exports.ScriptArgument = ScriptArgument; /** * Commands like 'echo' with a single string argument, and used with the * special format like: 'echo a b c' effectively have a number of arguments * merged together. */ function MergedArgument(args, start, end) { @@ -1401,63 +1403,72 @@ MergedArgument.prototype.equals = functi } // We might need to add a check that args is the same here return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; -argument.MergedArgument = MergedArgument; +exports.MergedArgument = MergedArgument; /** * TrueNamedArguments are for when we have an argument like --verbose which * has a boolean value, and thus the opposite of '--verbose' is ''. */ -function TrueNamedArgument(name, arg) { +function TrueNamedArgument(arg) { this.arg = arg; - this.text = arg ? arg.text : '--' + name; - this.prefix = arg ? arg.prefix : ' '; - this.suffix = arg ? arg.suffix : ''; + this.text = arg.text; + this.prefix = arg.prefix; + this.suffix = arg.suffix; } TrueNamedArgument.prototype = Object.create(Argument.prototype); TrueNamedArgument.prototype.type = 'TrueNamedArgument'; TrueNamedArgument.prototype.assign = function(assignment) { if (this.arg) { this.arg.assign(assignment); } this.assignment = assignment; }; TrueNamedArgument.prototype.getArgs = function() { - // NASTY! getArgs has a fairly specific use: in removing used arguments - // from a command line. Unlike other arguments which are EITHER used - // in assignments directly OR grouped in things like MergedArguments, - // TrueNamedArgument is used raw from the UI, or composed of another arg - // from the CLI, so we return both here so they can both be removed. - return this.arg ? [ this, this.arg ] : [ this ]; + return [ this.arg ]; }; TrueNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof TrueNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; -argument.TrueNamedArgument = TrueNamedArgument; +/** + * As Argument.beget that rebuilds nameArg and valueArg + */ +TrueNamedArgument.prototype.beget = function(options) { + if (options.text) { + console.error('Can\'t change text of a TrueNamedArgument', this, options); + } + + options.type = TrueNamedArgument; + var begotten = Argument.prototype.beget.call(this, options); + begotten.arg = new Argument(begotten.text, begotten.prefix, begotten.suffix); + return begotten; +}; + +exports.TrueNamedArgument = TrueNamedArgument; /** * FalseNamedArguments are for when we don't have an argument like --verbose * which has a boolean value, and thus the opposite of '' is '--verbose'. */ function FalseNamedArgument() { this.text = ''; @@ -1480,58 +1491,73 @@ FalseNamedArgument.prototype.equals = fu if (that == null || !(that instanceof FalseNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; -argument.FalseNamedArgument = FalseNamedArgument; +exports.FalseNamedArgument = FalseNamedArgument; /** * A named argument is for cases where we have input in one of the following * formats: * <ul> * <li>--param value * <li>-p value * </ul> * We model this as a normal argument but with a long prefix. - */ -function NamedArgument(nameArg, valueArg) { - this.nameArg = nameArg; - this.valueArg = valueArg; - - if (valueArg == null) { + * + * There are 2 ways to construct a NamedArgument. One using 2 Arguments which + * are taken to be the argument for the name (e.g. '--param') and one for the + * value to assign to that parameter. + * Alternatively, you can pass in the text/prefix/suffix values in the same + * way as an Argument is constructed. If you do this then you are expected to + * assign to nameArg and valueArg before exposing the new NamedArgument. + */ +function NamedArgument() { + if (typeof arguments[0] === 'string') { + this.nameArg = null; + this.valueArg = null; + this.text = arguments[0]; + this.prefix = arguments[1]; + this.suffix = arguments[2]; + } + else if (arguments[1] == null) { + this.nameArg = arguments[0]; + this.valueArg = null; this.text = ''; - this.prefix = nameArg.toString(); + this.prefix = this.nameArg.toString(); this.suffix = ''; } else { - this.text = valueArg.text; - this.prefix = nameArg.toString() + valueArg.prefix; - this.suffix = valueArg.suffix; + this.nameArg = arguments[0]; + this.valueArg = arguments[1]; + this.text = this.valueArg.text; + this.prefix = this.nameArg.toString() + this.valueArg.prefix; + this.suffix = this.valueArg.suffix; } } NamedArgument.prototype = Object.create(Argument.prototype); NamedArgument.prototype.type = 'NamedArgument'; NamedArgument.prototype.assign = function(assignment) { this.nameArg.assign(assignment); if (this.valueArg != null) { this.valueArg.assign(assignment); } this.assignment = assignment; }; NamedArgument.prototype.getArgs = function() { - return [ this.nameArg, this.valueArg ]; + return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ]; }; NamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; @@ -1542,17 +1568,40 @@ NamedArgument.prototype.equals = functio } // We might need to add a check that nameArg and valueArg are the same return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; -argument.NamedArgument = NamedArgument; +/** + * As Argument.beget that rebuilds nameArg and valueArg + */ +NamedArgument.prototype.beget = function(options) { + options.type = NamedArgument; + var begotten = Argument.prototype.beget.call(this, options); + + // Cut the prefix into |whitespace|non-whitespace|whitespace| so we can + // rebuild nameArg and valueArg from the parts + var matches = /^([\s]*)([^\s]*)([\s]*)$/.exec(begotten.prefix); + + if (this.valueArg == null && begotten.text === '') { + begotten.nameArg = new Argument(matches[2], matches[1], matches[3]); + begotten.valueArg = null; + } + else { + begotten.nameArg = new Argument(matches[2], matches[1], ''); + begotten.valueArg = new Argument(begotten.text, matches[3], begotten.suffix); + } + + return begotten; +}; + +exports.NamedArgument = NamedArgument; /** * An argument the groups together a number of plain arguments together so they * can be jointly assigned to a single array parameter */ function ArrayArgument() { this.args = []; @@ -1615,17 +1664,17 @@ ArrayArgument.prototype.equals = functio * Helper when we're putting arguments back together */ ArrayArgument.prototype.toString = function() { return '{' + this.args.map(function(arg) { return arg.toString(); }, this).join(',') + '}'; }; -argument.ArrayArgument = ArrayArgument; +exports.ArrayArgument = ArrayArgument; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2363,18 +2412,18 @@ function Command(commandSpec) { // 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 // prevents nesting. paramSpecs.forEach(function(spec) { if (!spec.group) { if (usingGroups) { - console.error('Parameters can\'t come after param groups.' + - ' Ignoring ' + this.name + '/' + spec.name); + throw new Error('Parameters can\'t come after param groups.' + + ' Ignoring ' + this.name + '/' + spec.name); } else { var param = new Parameter(spec, this, null); this.params.push(param); if (!param.isPositionalAllowed) { this.hasNamedParameters = true; } @@ -2407,69 +2456,69 @@ function Parameter(paramSpec, command, g this.paramSpec = paramSpec; this.name = this.paramSpec.name; this.type = this.paramSpec.type; this.groupName = groupName; this.defaultValue = this.paramSpec.defaultValue; if (!this.name) { throw new Error('In ' + this.command.name + - ': all params must have a name'); + ': all params must have a name'); } var typeSpec = this.type; this.type = types.getType(typeSpec); if (this.type == null) { console.error('Known types: ' + types.getTypeNames().join(', ')); throw new Error('In ' + this.command.name + '/' + this.name + - ': can\'t find type for: ' + JSON.stringify(typeSpec)); + ': can\'t find type for: ' + JSON.stringify(typeSpec)); } // boolean parameters have an implicit defaultValue:false, which should // not be changed. See the docs. if (this.type instanceof BooleanType) { if (this.defaultValue !== undefined) { - console.error('In ' + this.command.name + '/' + this.name + - ': boolean parameters can not have a defaultValue.' + - ' Ignoring'); + throw new Error('In ' + this.command.name + '/' + this.name + + ': boolean parameters can not have a defaultValue.' + + ' Ignoring'); } this.defaultValue = false; } // Check the defaultValue for validity. // Both undefined and null get a pass on this test. undefined is used when // there is no defaultValue, and null is used when the parameter is // optional, neither are required to parse and stringify. if (this.defaultValue != null) { try { var defaultText = this.type.stringify(this.defaultValue); var defaultConversion = this.type.parseString(defaultText); if (defaultConversion.getStatus() !== Status.VALID) { - console.error('In ' + this.command.name + '/' + this.name + - ': Error round tripping defaultValue. status = ' + - defaultConversion.getStatus()); + throw new Error('In ' + this.command.name + '/' + this.name + + ': Error round tripping defaultValue. status = ' + + defaultConversion.getStatus()); } } catch (ex) { - console.error('In ' + this.command.name + '/' + this.name + - ': ' + ex); + throw new Error('In ' + this.command.name + '/' + this.name + + ': ' + ex); } } // Some types (boolean, array) have a non 'undefined' blank value. Give the // type a chance to override the default defaultValue of undefined if (this.defaultValue === undefined) { this.defaultValue = this.type.getBlank().value; } // All parameters that can only be set via a named parameter must have a // non-undefined default value if (!this.isPositionalAllowed && this.defaultValue === undefined) { - console.error('In ' + this.command.name + '/' + this.name + - ': Missing defaultValue for optional parameter.'); + throw new Error('In ' + this.command.name + '/' + this.name + + ': Missing defaultValue for optional parameter.'); } } /** * Does the given name uniquely identify this param (among the other params * in this command) * @param name The name to check */ @@ -5099,16 +5148,57 @@ Assignment.prototype.getMessage = functi * @return An array of objects with name and value elements. For example: * [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ] */ Assignment.prototype.getPredictions = function() { return this.conversion.getPredictions(); }; /** + * Accessor for a prediction by index. + * This is useful above <tt>getPredictions()[index]</tt> because it normalizes + * index to be within the bounds of the predictions, which means that the UI + * can maintain an index of which prediction to choose without caring how many + * predictions there are. + * @param index The index of the prediction to choose + */ +Assignment.prototype.getPredictionAt = function(index) { + if (index == null) { + index = 0; + } + + if (this.isInName()) { + return undefined; + } + + var predictions = this.getPredictions(); + if (predictions.length === 0) { + return undefined; + } + + index = index % predictions.length; + if (index < 0) { + index = predictions.length + index; + } + return predictions[index]; +}; + +/** + * Some places want to take special action if we are in the name part of a + * named argument (i.e. the '--foo' bit). + * Currently this does not take actual cursor position into account, it just + * assumes that the cursor is at the end. In the future we will probably want + * to take this into account. + */ +Assignment.prototype.isInName = function() { + return this.conversion.arg.type === 'NamedArgument' && + this.conversion.arg.prefix.slice(-1) !== ' '; +}; + +/** * Report on the status of the last parse() conversion. * We force mutations to happen through this method rather than have * setValue and setArgument functions to help maintain integrity when we * have ArrayArguments and don't want to get confused. This way assignments * are just containers for a conversion rather than things that store * a connection between an arg/value. * @see types.Conversion */ @@ -5145,17 +5235,18 @@ Assignment.prototype.ensureVisibleArgume // It should only be called when structural changes are happening in which // case we're going to ignore the event anyway. But on the other hand // perhaps this function shouldn't need to know how it is used, and should // do the inefficient thing. if (this.conversion.arg.type !== 'BlankArgument') { return false; } - var arg = this.conversion.arg.beget('', { + var arg = this.conversion.arg.beget({ + text: '', prefixSpace: this.param instanceof CommandAssignment }); this.conversion = this.param.type.parse(arg); this.conversion.assign(this); return true; }; @@ -5177,42 +5268,16 @@ Assignment.prototype.getStatus = functio if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') { return Status.VALID; } return this.conversion.getStatus(arg); }; /** - * Replace the current value with the lower value if such a concept exists. - */ -Assignment.prototype.decrement = function() { - var replacement = this.param.type.decrement(this.conversion.value); - if (replacement != null) { - var str = this.param.type.stringify(replacement); - var arg = this.conversion.arg.beget(str); - var conversion = new Conversion(replacement, arg); - this.setConversion(conversion); - } -}; - -/** - * Replace the current value with the higher value if such a concept exists. - */ -Assignment.prototype.increment = function() { - var replacement = this.param.type.increment(this.conversion.value); - if (replacement != null) { - var str = this.param.type.stringify(replacement); - var arg = this.conversion.arg.beget(str); - var conversion = new Conversion(replacement, arg); - this.setConversion(conversion); - } -}; - -/** * Helper when we're rebuilding command lines. */ Assignment.prototype.toString = function() { return this.conversion.toString(); }; /** * For test/debug use only. The output from this function is subject to wanton @@ -5420,22 +5485,16 @@ function Requisition(environment, doc) { this.commandOutputManager = canon.commandOutputManager; this.onAssignmentChange = util.createEvent('Requisition.onAssignmentChange'); this.onTextChange = util.createEvent('Requisition.onTextChange'); } /** - * Some number that is higher than the most args we'll ever have. Would use - * MAX_INTEGER if that made sense - */ -var MORE_THAN_THE_MOST_ARGS_POSSIBLE = 1000000; - -/** * Avoid memory leaks */ Requisition.prototype.destroy = function() { this.commandAssignment.onAssignmentChange.remove(this._commandAssignmentChanged, this); this.commandAssignment.onAssignmentChange.remove(this._assignmentChanged, this); delete this.document; delete this.environment; @@ -5459,57 +5518,16 @@ Requisition.prototype._assignmentChanged this.onAssignmentChange(ev); // Both for argument position and the onTextChange event, we only care // about changes to the argument. if (ev.conversion.argEquals(ev.oldConversion)) { return; } - this._structuralChangeInProgress = true; - - // 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 (i = 0; i < ev.assignment.paramIndex; i++) { - var assignment = this.getAssignment(i); - if (assignment.param.isPositionalAllowed) { - if (assignment.ensureVisibleArgument()) { - this._args.push(assignment.arg); - } - } - } - } - - // Remember where we found the first match - var index = MORE_THAN_THE_MOST_ARGS_POSSIBLE; - for (i = 0; i < this._args.length; i++) { - if (this._args[i].assignment === ev.assignment) { - if (i < index) { - index = i; - } - this._args.splice(i, 1); - i--; - } - } - - if (index === MORE_THAN_THE_MOST_ARGS_POSSIBLE) { - this._args.push(ev.assignment.arg); - } - else { - // Is there a way to do this that doesn't involve a loop? - var newArgs = ev.conversion.arg.getArgs(); - for (i = 0; i < newArgs.length; i++) { - this._args.splice(index + i, 0, newArgs[i]); - } - } - this._structuralChangeInProgress = false; - this.onTextChange(); }; /** * When the command changes, we need to keep a bunch of stuff in sync */ Requisition.prototype._commandAssignmentChanged = function(ev) { // Assignments fire AssignmentChange events on any change, including minor @@ -5541,16 +5559,38 @@ Requisition.prototype._commandAssignment Requisition.prototype.getAssignment = function(nameOrNumber) { var name = (typeof nameOrNumber === 'string') ? nameOrNumber : Object.keys(this._assignments)[nameOrNumber]; return this._assignments[name] || undefined; }; /** + * There are a few places where we need to know what the 'next thing' is. What + * is the user going to be filling out next (assuming they don't enter a named + * argument). The next argument is the first in line that is both blank, and + * that can be filled in positionally. + * @return The next assignment to be used, or null if all the positional + * parameters have values. + */ +Requisition.prototype._getFirstBlankPositionalAssignment = function() { + var reply = null; + Object.keys(this._assignments).some(function(name) { + var assignment = this.getAssignment(name); + if (assignment.arg.type === 'BlankArgument' && + assignment.param.isPositionalAllowed) { + reply = assignment; + return true; // i.e. break + } + return false; + }, this); + return reply; +}; + +/** * Where parameter name == assignment names - they are the same */ Requisition.prototype.getParameterNames = function() { return Object.keys(this._assignments); }; /** * A *shallow* clone of the assignments. @@ -5626,29 +5666,44 @@ Requisition.prototype.getAssignments = f }; /** * Alter the given assignment using the given arg. This function is better than * calling assignment.setConversion(assignment.param.type.parse(arg)) because * it adjusts the args in this requisition to keep things up to date */ Requisition.prototype.setAssignment = function(assignment, arg) { - var originalArg = assignment.arg; + var originalArgs = assignment.arg.getArgs(); var conversion = assignment.param.type.parse(arg); assignment.setConversion(conversion); - // If this argument isn't assigned to anything (i.e. it was created by - // assignment.setBlank) we need to add it into the _args array so - // requisition.toString can make sense - if (originalArg.type === 'BlankArgument') { - this._args.push(arg); - } - else { - var index = this._args.indexOf(originalArg); - this._args[index] = conversion.arg; + var replacementArgs = arg.getArgs(); + var maxLen = Math.max(originalArgs.length, replacementArgs.length); + for (var i = 0; i < maxLen; i++) { + // If there are no more original args, or if the original arg was blank + // (i.e. not typed by the user), we'll just need to add at the end + if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') { + this._args.push(replacementArgs[i]); + continue; + } + + var index = this._args.indexOf(originalArgs[i]); + if (index === -1) { + console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args); + throw new Error('Couldn\'t find ' + originalArgs[i]); + } + + // If there are no more replacement args, we just remove the original args + // Otherwise swap original args and replacements + if (i >= replacementArgs.length) { + this._args.splice(index, 1); + } + else { + this._args[index] = replacementArgs[i]; + } } }; /** * Reset all the assignments to their default values */ Requisition.prototype.setBlankArguments = function() { this.getAssignments().forEach(function(assignment) { @@ -5667,74 +5722,124 @@ Requisition.prototype.setBlankArguments * which should be set to start and end of the selection. * @param predictionChoice The index of the prediction that we should choose. * This number is not bounded by the size of the prediction array, we take the * modulus to get it within bounds */ Requisition.prototype.complete = function(cursor, predictionChoice) { var assignment = this.getAssignmentAt(cursor.start); - var predictions = assignment.conversion.getPredictions(); - if (predictions.length > 0) { - this.onTextChange.holdFire(); - - var prediction = assignment.conversion.getPredictionAt(predictionChoice); - + this.onTextChange.holdFire(); + + var prediction = assignment.getPredictionAt(predictionChoice); + if (prediction == null) { + // No predictions generally means we shouldn't change anything on TAB, but + // TAB has the connotation of 'next thing' and when we're at the end of + // a thing that implies that we should add a space. i.e. + // 'help<TAB>' -> 'help ' + // But we should only do this if the thing that we're 'completing' is valid + // and doesn't already end in a space. + if (assignment.arg.suffix.slice(-1) !== ' ' && + assignment.getStatus() === Status.VALID) { + this._addSpace(assignment); + } + + // Also add a space if we are in the name part of an assignment, however + // this time we don't want the 'push the space to the next assignment' + // logic, so we don't use addSpace + if (assignment.isInName()) { + var newArg = assignment.conversion.arg.beget({ prefixPostSpace: true }); + this.setAssignment(assignment, newArg); + } + } + else { // Mutate this argument to hold the completion - var arg = assignment.arg.beget(prediction.name); + var arg = assignment.arg.beget({ text: prediction.name }); this.setAssignment(assignment, arg); - if (prediction.incomplete) { - // This is the easy case - the prediction is incomplete - no need to add - // any spaces - return; - } - - // The prediction reported !incomplete, which means it's complete so we - // should add a space to delimit this argument and let the user move-on. - // The question is, where does the space go? The obvious thing to do is to - // add it to the suffix of the completed argument, but that's wrong because - // spaces are attached to the start of the next argument rather than the - // end of the previous one (and this matters to getCurrentAssignment). - // However there might not be a next argument (if we've at the end of the - // input), in which case we really do use this one. - // Also if there is already a space in those positions, don't add another - - var nextIndex = assignment.paramIndex + 1; - var nextAssignment = this.getAssignment(nextIndex); - if (nextAssignment) { - // Add a space onto the next argument (if there isn't one there already) - var nextArg = nextAssignment.conversion.arg; - if (nextArg.prefix.charAt(0) !== ' ') { - nextArg = new Argument(nextArg.text, ' ' + nextArg.prefix, nextArg.suffix); - this.setAssignment(nextAssignment, nextArg); + if (!prediction.incomplete) { + // The prediction is complete, add a space to let the user move-on + this._addSpace(assignment); + + // Bug 779443 - Remove or explain the reparse + if (assignment instanceof UnassignedAssignment) { + this.update(this.toString()); } } - else { - // There is no next argument, this must be the last assignment, so just - // add the space to the prefix of this argument - arg = assignment.conversion.arg; - if (arg.suffix.charAt(arg.suffix.length - 1) !== ' ') { - // It's tempting to think - "we're calling setAssignment twice in one - // call to complete, the first time to complete the text, the second - // to add a space, why not save the event cascade and do it once" - // However if we're setting up the command, the number of parameters - // changes as a result, so our call to getAssignment(nextIndex) will - // produce the wrong answer - arg = new Argument(arg.text, arg.prefix, arg.suffix + ' '); - this.setAssignment(assignment, arg); - } - } - - if (assignment instanceof UnassignedAssignment) { - this.update(this.toString()); - } - - this.onTextChange(); - this.onTextChange.resumeFire(); + } + + this.onTextChange(); + this.onTextChange.resumeFire(); +}; + +/** + * Pressing TAB sometimes requires that we add a space to denote that we're on + * to the 'next thing'. + * The question is, where does the space go? The obvious thing to do is to add + * it to the suffix of the completed argument, but that's wrong because spaces + * are attached to the start of the next argument rather than the end of the + * previous one (and this matters to getCurrentAssignment). + * However there might not be a 'next' argument (if we've at the end of the + * input), in which case we really do use this one. + * Also if there is already a space in those positions, don't add another + * In addition to all of this, we need to know what the 'next' argument is. + * We can't use the argument defined just after the thing that is being + * completed, because we could be completing a named argument, so we need to + * look for the first blank positional parameter, but if there isn't one of + * those then we just add to the suffix of the current. + * @param assignment The 'last' assignment to which to append the space if + * there is no 'next' assignment to which we can prepend a space + */ +Requisition.prototype._addSpace = function(assignment) { + var nextAssignment = this._getFirstBlankPositionalAssignment(); + if (nextAssignment) { + // Add a space onto the next argument (if there isn't one there already) + var nextArg = nextAssignment.conversion.arg; + if (nextArg.prefix.charAt(0) !== ' ') { + nextArg = new Argument(nextArg.text, ' ' + nextArg.prefix, nextArg.suffix); + this.setAssignment(nextAssignment, nextArg); + } + } + else { + // There is no next argument, this must be the last assignment, so just + // add the space to the prefix of this argument + var newArg = assignment.conversion.arg.beget({ suffixSpace: true }); + if (newArg !== assignment.conversion.arg) { + // It's tempting to think - "we're calling setAssignment twice in one + // call to complete, the first time to complete the text, the second + // to add a space, why not save the event cascade and do it once" + // However if we're setting up the command, the number of parameters + // changes as a result, so our call to getFirstBlankPositionalAssignment + // will produce the wrong answer + this.setAssignment(assignment, newArg); + } + } +}; + +/** + * Replace the current value with the lower value if such a concept exists. + */ +Requisition.prototype.decrement = function(assignment) { + var replacement = assignment.param.type.decrement(assignment.conversion.value); + if (replacement != null) { + var str = assignment.param.type.stringify(replacement); + var arg = assignment.conversion.arg.beget({ text: str }); + this.setAssignment(assignment, arg); + } +}; + +/** + * Replace the current value with the higher value if such a concept exists. + */ +Requisition.prototype.increment = function(assignment) { + var replacement = assignment.param.type.increment(assignment.conversion.value); + if (replacement != null) { + var str = assignment.param.type.stringify(replacement); + var arg = assignment.conversion.arg.beget({ text: str }); + this.setAssignment(assignment, arg); } }; /** * Extract a canonical version of the input */ Requisition.prototype.toCanonicalString = function() { var line = []; @@ -5944,19 +6049,22 @@ Requisition.prototype.getAssignmentAt = // otherwise it looks forwards if (arg.assignment.arg.type === 'NamedArgument') { // leave the argument as it is } else if (this._args.length > i + 1) { // first to the next argument assignment = this._args[i + 1].assignment; } - else if (assignment && assignment.paramIndex + 1 < this.assignmentCount) { - // then to the next assignment - assignment = this.getAssignment(assignment.paramIndex + 1); + else { + // then to the first blank positional parameter, leaving 'as is' if none + var nextAssignment = this._getFirstBlankPositionalAssignment(); + if (nextAssignment != null) { + assignment = nextAssignment; + } } for (j = 0; j < arg.suffix.length; j++) { assignForPos.push(assignment); } } // Possible shortcut, we don't really need to go through all the args @@ -6457,17 +6565,17 @@ Requisition.prototype._assign = function if (assignment.param.isKnownAs(args[i].text)) { var arg = args.splice(i, 1)[0]; unassignedParams = unassignedParams.filter(function(test) { return test !== assignment.param.name; }); // boolean parameters don't have values, default to false if (assignment.param.type instanceof BooleanType) { - arg = new TrueNamedArgument(null, arg); + arg = new TrueNamedArgument(arg); } else { var valueArg = null; if (i + 1 <= args.length) { valueArg = args.splice(i, 1)[0]; } arg = new NamedArgument(arg, valueArg); } @@ -7222,17 +7330,17 @@ StringField.prototype.destroy = function StringField.prototype.setConversion = function(conversion) { this.arg = conversion.arg; this.element.value = conversion.arg.text; this.setMessage(conversion.message); }; StringField.prototype.getConversion = function() { // This tweaks the prefix/suffix of the argument to fit - this.arg = this.arg.beget(this.element.value, { prefixSpace: true }); + this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true }); return this.type.parse(this.arg); }; StringField.claim = function(type) { return type instanceof StringType ? Field.MATCH : Field.BASIC; }; @@ -7277,17 +7385,17 @@ NumberField.prototype.destroy = function NumberField.prototype.setConversion = function(conversion) { this.arg = conversion.arg; this.element.value = conversion.arg.text; this.setMessage(conversion.message); }; NumberField.prototype.getConversion = function() { - this.arg = this.arg.beget(this.element.value, { prefixSpace: true }); + this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true }); return this.type.parse(this.arg); }; /** * A field that uses a checkbox to toggle a boolean field */ function BooleanField(type, options) { @@ -7324,17 +7432,17 @@ BooleanField.prototype.setConversion = f this.element.checked = conversion.value; this.setMessage(conversion.message); }; BooleanField.prototype.getConversion = function() { var arg; if (this.named) { arg = this.element.checked ? - new TrueNamedArgument(this.name) : + new TrueNamedArgument(new Argument(' --' + this.name)) : new FalseNamedArgument(); } else { arg = new Argument(' ' + this.element.checked); } return this.type.parse(arg); }; @@ -7776,17 +7884,17 @@ exports.addField(BlankField); * limitations under the License. */ define('gcli/ui/fields/javascript', ['require', 'exports', 'module' , 'gcli/util', 'gcli/argument', 'gcli/types/javascript', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) { var util = require('gcli/util'); -var Argument = require('gcli/argument').Argument; +var ScriptArgument = require('gcli/argument').ScriptArgument; var JavascriptType = require('gcli/types/javascript').JavascriptType; var Menu = require('gcli/ui/fields/menu').Menu; var Field = require('gcli/ui/fields').Field; var fields = require('gcli/ui/fields'); /** @@ -7803,17 +7911,17 @@ exports.shutdown = function() { /** * A field that allows editing of javascript */ function JavascriptField(type, options) { Field.call(this, type, options); this.onInputChange = this.onInputChange.bind(this); - this.arg = new Argument('', '{ ', ' }'); + this.arg = new ScriptArgument('', '{ ', ' }'); this.element = util.createElement(this.document, 'div'); this.input = util.createElement(this.document, 'input'); this.input.type = 'text'; this.input.addEventListener('keyup', this.onInputChange, false); this.input.classList.add('gcli-field'); this.input.classList.add('gcli-field-javascript'); @@ -7821,17 +7929,17 @@ function JavascriptField(type, options) this.menu = new Menu({ document: this.document, field: true, type: type }); this.element.appendChild(this.menu.element); - this.setConversion(this.type.parse(new Argument(''))); + this.setConversion(this.type.parse(new ScriptArgument(''))); this.onFieldChange = util.createEvent('JavascriptField.onFieldChange'); // i.e. Register this.onItemClick as the default action for a menu click this.menu.onItemClick.add(this.itemClicked, this); } JavascriptField.prototype = Object.create(Field.prototype); @@ -7891,17 +7999,17 @@ JavascriptField.prototype.onInputChange this.item = ev.currentTarget.item; var conversion = this.getConversion(); this.onFieldChange({ conversion: conversion }); this.setMessage(conversion.message); }; JavascriptField.prototype.getConversion = function() { // This tweaks the prefix/suffix of the argument to fit - this.arg = this.arg.beget(this.input.value, { normalize: true }); + this.arg = new ScriptArgument(this.input.value, '{ ', ' }'); return this.type.parse(this.arg); }; JavascriptField.DEFAULT_VALUE = '__JavascriptField.DEFAULT_VALUE'; }); /* @@ -8261,17 +8369,17 @@ SelectionField.prototype._addOption = fu var option = util.createElement(this.document, 'option'); option.innerHTML = item.name; option.value = item.index; this.element.appendChild(option); }; /** - * A field that allows editing of javascript + * A field that allows selection of one of a number of options */ function SelectionTooltipField(type, options) { Field.call(this, type, options); this.onInputChange = this.onInputChange.bind(this); this.arg = new Argument(); this.menu = new Menu({ document: this.document, type: type }); @@ -8319,17 +8427,17 @@ SelectionTooltipField.prototype.onInputC this.item = ev.currentTarget.item; var conversion = this.getConversion(); this.onFieldChange({ conversion: conversion }); this.setMessage(conversion.message); }; SelectionTooltipField.prototype.getConversion = function() { // This tweaks the prefix/suffix of the argument to fit - this.arg = this.arg.beget('typed', { normalize: true }); + this.arg = this.arg.beget({ text: this.input.value }); return this.type.parse(this.arg); }; /** * Allow the menu to highlight the correct prediction choice */ SelectionTooltipField.prototype.setChoiceIndex = function(choice) { this.menu.setChoiceIndex(choice); @@ -9416,17 +9524,17 @@ Inputter.prototype.onKeyUp = function(ev else if (this.element.value === '' || this._scrollingThroughHistory) { this._scrollingThroughHistory = true; this.requisition.update(this.history.backward()); } else { // If the user is on a valid value, then we increment the value, but if // they've typed something that's not right we page through predictions if (this.assignment.getStatus() === Status.VALID) { - this.assignment.increment(); + this.requisition.increment(assignment); // See notes on focusManager.onInputChange in onKeyDown if (this.focusManager) { this.focusManager.onInputChange(ev); } } else { this.changeChoice(-1); } @@ -9440,17 +9548,17 @@ Inputter.prototype.onKeyUp = function(ev } else if (this.element.value === '' || this._scrollingThroughHistory) { this._scrollingThroughHistory = true; this.requisition.update(this.history.forward()); } else { // See notes above for the UP key if (this.assignment.getStatus() === Status.VALID) { - this.assignment.decrement(); + this.requisition.decrement(assignment); // See notes on focusManager.onInputChange in onKeyDown if (this.focusManager) { this.focusManager.onInputChange(ev); } } else { this.changeChoice(+1); } @@ -9777,118 +9885,131 @@ Completer.prototype.update = function(ev Completer.prototype._getCompleterTemplateData = function() { var input = this.inputter.getInputState(); // directTabText is for when the current input is a prefix of the completion // arrowTabText is for when we need to use an -> to show what will be used var directTabText = ''; var arrowTabText = ''; var current = this.requisition.getAssignmentAt(input.cursor.start); + var emptyParameters = []; if (input.typed.trim().length !== 0) { - var prediction = current.conversion.getPredictionAt(this.choice); + var cArg = current.arg; + var prediction = current.getPredictionAt(this.choice); + if (prediction) { var tabText = prediction.name; - var existing = current.arg.text; + var existing = cArg.text; + + // Normally the cursor being just before whitespace means that you are + // 'in' the previous argument, which means that the prediction is based + // on that argument, however NamedArguments break this by having 2 parts + // so we need to prepend the tabText with a space for NamedArguments, + // but only when there isn't already a space at the end of the prefix + // (i.e. ' --name' not ' --name ') + if (current.isInName()) { + tabText = ' ' + tabText; + } if (existing !== tabText) { // Decide to use directTabText or arrowTabText // Strip any leading whitespace from the user inputted value because the // tabText will never have leading whitespace. var inputValue = existing.replace(/^\s*/, ''); var isStrictCompletion = tabText.indexOf(inputValue) === 0; if (isStrictCompletion && input.cursor.start === input.typed.length) { // Display the suffix of the prediction as the completion var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; directTabText = tabText.slice(existing.length - numLeadingSpaces); } else { // Display the '-> prediction' at the end of the completer element - // These JS escapes are aka → the right arrow - arrowTabText = ' \u00a0\u21E5 ' + tabText; + // \u21E5 is the JS escape right arrow + arrowTabText = '\u21E5 ' + tabText; } } } + else { + // There's no prediction, but if this is a named argument that needs a + // value (that is without any) then we need to show that one is needed + // For example 'git commit --message ', clearly needs some more text + if (cArg.type === 'NamedArgument' && cArg.text === '') { + emptyParameters.push('<' + current.param.type.name + '>\u00a0'); + } + } + } + + // Add a space between the typed text (+ directTabText) and the hints, + // making sure we don't add 2 sets of padding + if (directTabText !== '') { + directTabText += '\u00a0'; + } + else if (!this.requisition.typedEndsWithSeparator()) { + emptyParameters.unshift('\u00a0'); } // statusMarkup is wrapper around requisition.getInputStatusMarkup converting // space to in the string member (for HTML display) and status to an // appropriate class name (i.e. lower cased, prefixed with gcli-in-) var statusMarkup = this.requisition.getInputStatusMarkup(input.cursor.start); statusMarkup.forEach(function(member) { member.string = member.string.replace(/ /g, '\u00a0'); // i.e. member.className = 'gcli-in-' + member.status.toString().toLowerCase(); }, this); // Calculate the list of parameters to be filled in - var trailingSeparator = this.requisition.typedEndsWithSeparator(); // We generate an array of emptyParameter markers for each positional // parameter to the current command. // Generally each emptyParameter marker begins with a space to separate it // from whatever came before, unless what comes before ends in a space. - // Also if we've got a directTabText prediction or we're in a NamedParameter - // then we don't want any text for that parameter at all. - // The algorithm to add spaces needs to take this into account. var command = this.requisition.commandAssignment.value; var jsCommand = command && command.name === '{'; - var firstBlankParam = true; - var emptyParameters = []; this.requisition.getAssignments().forEach(function(assignment) { + // Named arguments are handled with a group [options] marker if (!assignment.param.isPositionalAllowed) { return; } - if (current.arg.type === 'NamedArgument') { - return; - } - + + // No hints if we've got content for this parameter if (assignment.arg.toString().trim() !== '') { - if (directTabText !== '') { - firstBlankParam = false; - } return; } - if (directTabText !== '' && firstBlankParam) { - firstBlankParam = false; + if (directTabText !== '' && current === assignment) { return; } var text = (assignment.param.isDataRequired) ? - '<' + assignment.param.name + '>' : - '[' + assignment.param.name + ']'; - - // Add a space if we don't have one at the end of the input or if - // this isn't the first param we've mentioned - if (!trailingSeparator || !firstBlankParam) { - text = '\u00a0' + text; // i.e. - } - - firstBlankParam = false; + '<' + assignment.param.name + '>\u00a0' : + '[' + assignment.param.name + ']\u00a0'; + emptyParameters.push(text); }.bind(this)); - var optionsRemaining = false; + var addOptionsMarker = false; + // We add an '[options]' marker when there are named parameters that are + // not filled in and not hidden, and we don't have any directTabText if (command && command.hasNamedParameters) { command.params.forEach(function(param) { var arg = this.requisition.getAssignment(param.name).arg; if (!param.isPositionalAllowed && !param.hidden && arg.type === "BlankArgument") { - optionsRemaining = true; + addOptionsMarker = true; } }, this); } - if (optionsRemaining) { + if (addOptionsMarker) { // Add an nbsp if we don't have one at the end of the input or if // this isn't the first param we've mentioned - var prefix = (!trailingSeparator || !firstBlankParam) ? '\u00a0' : ''; - emptyParameters.push(prefix + '[options]'); + emptyParameters.push('[options]\u00a0'); } // Is the entered command a JS command with no closing '}'? // TWEAK: This code should be considered for promotion to Requisition var unclosedJs = jsCommand && this.requisition.getAssignment(0).arg.suffix.indexOf('}') === -1; // The text for the 'jump to scratchpad' feature, or '' if it is disabled
--- a/browser/devtools/commandline/test/Makefile.in +++ b/browser/devtools/commandline/test/Makefile.in @@ -21,17 +21,17 @@ MOCHITEST_BROWSER_FILES = \ browser_cmd_integrate.js \ browser_cmd_jsb.js \ browser_cmd_pagemod_export.js \ browser_cmd_pref.js \ browser_cmd_restart.js \ browser_cmd_settings.js \ browser_gcli_web.js \ head.js \ - helper.js \ + helpers.js \ $(NULL) MOCHITEST_BROWSER_FILES += \ browser_dbg_cmd_break.html \ browser_dbg_cmd.html \ browser_cmd_pagemod_export.html \ browser_cmd_jsb_script.jsi \ $(NULL)
--- a/browser/devtools/commandline/test/browser_cmd_addon.js +++ b/browser/devtools/commandline/test/browser_cmd_addon.js @@ -3,48 +3,92 @@ // Tests that the addon commands works as they should function test() { DeveloperToolbarTest.test("about:blank", [ GAT_test ]); } function GAT_test() { + var GAT_ready = DeveloperToolbarTest.checkCalled(function() { + Services.obs.removeObserver(GAT_ready, "gcli_addon_commands_ready", false); + + helpers.setInput('addon list dictionary'); + helpers.check({ + input: 'addon list dictionary', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon list extension'); + helpers.check({ + input: 'addon list extension', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon list locale'); + helpers.check({ + input: 'addon list locale', + hints: '', + markup: 'VVVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon list plugin'); + helpers.check({ + input: 'addon list plugin', + hints: '', + markup: 'VVVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon list theme'); + helpers.check({ + input: 'addon list theme', + hints: '', + markup: 'VVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon list all'); + helpers.check({ + input: 'addon list all', + hints: '', + markup: 'VVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon disable Test_Plug-in_1.0.0.0'); + helpers.check({ + input: 'addon disable Test_Plug-in_1.0.0.0', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' + }); + + helpers.setInput('addon disable WRONG'); + helpers.check({ + input: 'addon disable WRONG', + hints: '', + markup: 'VVVVVVVVVVVVVVEEEEE', + status: 'ERROR' + }); + + helpers.setInput('addon enable Test_Plug-in_1.0.0.0'); + helpers.check({ + input: 'addon enable Test_Plug-in_1.0.0.0', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + command: { name: 'addon enable' }, + name: { value: 'Test Plug-in', status: 'VALID' }, + } + }); + + DeveloperToolbarTest.exec({ completed: false }); + }); + Services.obs.addObserver(GAT_ready, "gcli_addon_commands_ready", false); } - -var GAT_ready = DeveloperToolbarTest.checkCalled(function() { - Services.obs.removeObserver(GAT_ready, "gcli_addon_commands_ready", false); - - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list dictionary", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list extension", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list locale", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list plugin", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list theme", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon list all", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon disable Test_Plug-in_1.0.0.0", - status: "VALID" - }); - DeveloperToolbarTest.checkInputStatus({ - typed: "addon enable Test_Plug-in_1.0.0.0", - status: "VALID" - }); - DeveloperToolbarTest.exec({ completed: false }); -});
--- a/browser/devtools/commandline/test/browser_cmd_calllog.js +++ b/browser/devtools/commandline/test/browser_cmd_calllog.js @@ -8,43 +8,50 @@ Components.utils.import("resource:///mod const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog"; function test() { DeveloperToolbarTest.test(TEST_URI, [ testCallLogStatus, testCallLogExec ]); } function testCallLogStatus() { - DeveloperToolbarTest.checkInputStatus({ - typed: "calllog", - status: "ERROR" + helpers.setInput('calllog'); + helpers.check({ + input: 'calllog', + hints: '', + markup: 'IIIIIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "calllog start", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('calllog start'); + helpers.check({ + input: 'calllog start', + hints: '', + markup: 'VVVVVVVVVVVVV', + status: 'VALID' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "calllog start", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('calllog stop'); + helpers.check({ + input: 'calllog stop', + hints: '', + markup: 'VVVVVVVVVVVV', + status: 'VALID' }); } function testCallLogExec() { DeveloperToolbarTest.exec({ typed: "calllog stop", args: { }, outputMatch: /No call logging/, }); let hud = null; - function onWebConsoleOpen(aSubject) { + var onWebConsoleOpen = DeveloperToolbarTest.checkCalled(function(aSubject) { Services.obs.removeObserver(onWebConsoleOpen, "web-console-created"); aSubject.QueryInterface(Ci.nsISupportsString); hud = imported.HUDService.getHudReferenceById(aSubject.data); ok(hud.hudId in imported.HUDService.hudReferences, "console open"); DeveloperToolbarTest.exec({ typed: "calllog stop", @@ -61,17 +68,17 @@ function testCallLogExec() { let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output"); is(labels.length, 0, "no output in console"); DeveloperToolbarTest.exec({ typed: "console close", args: {}, blankOutput: true, }); - } + }); Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false); DeveloperToolbarTest.exec({ typed: "calllog start", args: { }, outputMatch: /Call logging started/, });
--- a/browser/devtools/commandline/test/browser_cmd_cookie.js +++ b/browser/devtools/commandline/test/browser_cmd_cookie.js @@ -1,50 +1,83 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Tests that the cookie commands works as they should const TEST_URI = "data:text/html;charset=utf-8,gcli-cookie"; function test() { - DeveloperToolbarTest.test(TEST_URI, [ testCookieCommands ]); + DeveloperToolbarTest.test(TEST_URI, [ testCookieCheck, testCookieExec ]); } -function testCookieCommands() { - DeveloperToolbarTest.checkInputStatus({ - typed: "cook", - directTabText: "ie", - status: "ERROR" +function testCookieCheck() { + helpers.setInput('cookie'); + helpers.check({ + input: 'cookie', + hints: '', + markup: 'IIIIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "cookie l", - directTabText: "ist", - status: "ERROR" + helpers.setInput('cookie lis'); + helpers.check({ + input: 'cookie lis', + hints: 't', + markup: 'IIIIIIVIII', + status: 'ERROR' + }); + + helpers.setInput('cookie list'); + helpers.check({ + input: 'cookie list', + hints: '', + markup: 'VVVVVVVVVVV', + status: 'VALID' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "cookie list", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('cookie remove'); + helpers.check({ + input: 'cookie remove', + hints: ' <key>', + markup: 'VVVVVVVVVVVVV', + status: 'ERROR' + }); + + helpers.setInput('cookie set'); + helpers.check({ + input: 'cookie set', + hints: ' <key> <value> [options]', + markup: 'VVVVVVVVVV', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "cookie remove", - status: "ERROR", - emptyParameters: [ " <key>" ] + helpers.setInput('cookie set fruit'); + helpers.check({ + input: 'cookie set fruit', + hints: ' <value> [options]', + markup: 'VVVVVVVVVVVVVVVV', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "cookie set", - status: "ERROR", - emptyParameters: [ " <key>", " <value>", " [options]" ], + helpers.setInput('cookie set fruit ban'); + helpers.check({ + input: 'cookie set fruit ban', + hints: ' [options]', + markup: 'VVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + key: { value: 'fruit' }, + value: { value: 'ban' }, + secure: { value: false }, + } }); +} +function testCookieExec() { DeveloperToolbarTest.exec({ typed: "cookie set fruit banana", args: { key: "fruit", value: "banana", path: "/", domain: null, secure: false
--- a/browser/devtools/commandline/test/browser_cmd_jsb.js +++ b/browser/devtools/commandline/test/browser_cmd_jsb.js @@ -2,50 +2,56 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ // Tests that the jsb command works as it should const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" + "test/browser_cmd_jsb_script.jsi"; function test() { - DeveloperToolbarTest.test("about:blank", [ GJT_test ]); + DeveloperToolbarTest.test("about:blank", [ /*GJT_test*/ ]); } function GJT_test() { - DeveloperToolbarTest.exec({ - typed: "jsb AAA", - outputMatch: /valid/ + helpers.setInput('jsb'); + helpers.check({ + input: 'jsb', + hints: ' <url> [indentSize] [indentChar] [preserveNewlines] [preserveMaxNewlines] [jslintHappy] [braceStyle] [spaceBeforeConditional] [unescapeStrings]', + markup: 'VVV', + status: 'ERROR' }); gBrowser.addTabsProgressListener({ onProgressChange: DeveloperToolbarTest.checkCalled(function GJT_onProgressChange(aBrowser) { gBrowser.removeTabsProgressListener(this); let win = aBrowser._contentWindow; let uri = win.document.location.href; let result = win.atob(uri.replace(/.*,/, "")); result = result.replace(/[\r\n]]/g, "\n"); - checkResult(result); + let correct = "function somefunc() {\n" + + " for (let n = 0; n < 500; n++) {\n" + + " if (n % 2 == 1) {\n" + + " console.log(n);\n" + + " console.log(n + 1);\n" + + " }\n" + + " }\n" + + "}"; + is(result, correct, "JS has been correctly prettified"); }) }); info("Checking beautification"); - DeveloperToolbarTest.checkInputStatus({ - typed: "jsb " + TEST_URI + " 4 space true -1 false collapse true false", - status: "VALID" - }); - DeveloperToolbarTest.exec({ completed: false }); - function checkResult(aResult) { - let correct = "function somefunc() {\n" + - " for (let n = 0; n < 500; n++) {\n" + - " if (n % 2 == 1) {\n" + - " console.log(n);\n" + - " console.log(n + 1);\n" + - " }\n" + - " }\n" + - "}"; - is(aResult, correct, "JS has been correctly prettified"); - } + helpers.setInput('jsb ' + TEST_URI); + /* + helpers.check({ + input: 'jsb', + hints: ' [options]', + markup: 'VVV', + status: 'VALID' + }); + */ + + DeveloperToolbarTest.exec({ completed: false }); }
--- a/browser/devtools/commandline/test/browser_cmd_pagemod_export.js +++ b/browser/devtools/commandline/test/browser_cmd_pagemod_export.js @@ -17,19 +17,22 @@ function test() { testPageModRemoveAttribute ]); function init() { initialHtml = content.document.documentElement.innerHTML; } function testExportHtml() { - DeveloperToolbarTest.checkInputStatus({ - typed: "export html", - status: "VALID" + helpers.setInput('export html'); + helpers.check({ + input: 'export html', + hints: '', + markup: 'VVVVVVVVVVV', + status: 'VALID' }); let oldOpen = content.open; let openURL = ""; content.open = function(aUrl) { openURL = aUrl; }; @@ -48,43 +51,46 @@ function test() { return content.document.documentElement.innerHTML; } function resetContent() { content.document.documentElement.innerHTML = initialHtml; } function testPageModReplace() { - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod replace", - emptyParameters: [" <search>", " <replace>", " [ignoreCase]", - " [selector]", " [root]", " [attrOnly]", - " [contentOnly]", " [attributes]"], - status: "ERROR" + helpers.setInput('pagemod replace'); + helpers.check({ + input: 'pagemod replace', + hints: ' <search> <replace> [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]', + markup: 'VVVVVVVVVVVVVVV', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod replace some foo", - emptyParameters: [" [ignoreCase]", " [selector]", " [root]", - " [attrOnly]", " [contentOnly]", " [attributes]"], - status: "VALID" + helpers.setInput('pagemod replace some foo'); + helpers.check({ + input: 'pagemod replace some foo', + hints: ' [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod replace some foo true", - emptyParameters: [" [selector]", " [root]", " [attrOnly]", - " [contentOnly]", " [attributes]"], - status: "VALID" + helpers.setInput('pagemod replace some foo true'); + helpers.check({ + input: 'pagemod replace some foo true', + hints: ' [selector] [root] [attrOnly] [contentOnly] [attributes]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod replace some foo true --attrOnly", - emptyParameters: [" [selector]", " [root]", " [contentOnly]", - " [attributes]"], - status: "VALID" + helpers.setInput('pagemod replace some foo true --attrOnly'); + helpers.check({ + input: 'pagemod replace some foo true --attrOnly', + hints: ' [selector] [root] [contentOnly] [attributes]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' }); DeveloperToolbarTest.exec({ typed: "pagemod replace sOme foOBar", outputMatch: /^[^:]+: 13\. [^:]+: 0\. [^:]+: 0\.\s*$/ }); is(getContent(), initialHtml, "no change in the page"); @@ -141,31 +147,38 @@ function test() { ".someclass changed to .foobarclass"); isnot(getContent().indexOf('<p id="someid">#someid'), -1, "#someid did not change"); resetContent(); } function testPageModRemoveElement() { - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod remove", - status: "ERROR" + helpers.setInput('pagemod remove'); + helpers.check({ + input: 'pagemod remove', + hints: '', + markup: 'IIIIIIIVIIIIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod remove element", - emptyParameters: [" <search>", " [root]", " [stripOnly]", " [ifEmptyOnly]"], - status: "ERROR" + helpers.setInput('pagemod remove element'); + helpers.check({ + input: 'pagemod remove element', + hints: ' <search> [root] [stripOnly] [ifEmptyOnly]', + markup: 'VVVVVVVVVVVVVVVVVVVVVV', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod remove element foo", - emptyParameters: [" [root]", " [stripOnly]", " [ifEmptyOnly]"], - status: "VALID" + helpers.setInput('pagemod remove element foo'); + helpers.check({ + input: 'pagemod remove element foo', + hints: ' [root] [stripOnly] [ifEmptyOnly]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' }); DeveloperToolbarTest.exec({ typed: "pagemod remove element p", outputMatch: /^[^:]+: 3\. [^:]+: 3\.\s*$/ }); is(getContent().indexOf('<p class="someclass">'), -1, "p.someclass removed"); @@ -210,26 +223,42 @@ function test() { isnot(getContent().indexOf(".someclass"), -1, ".someclass still exists"); isnot(getContent().indexOf("#someid"), -1, "#someid still exists"); isnot(getContent().indexOf("<strong>p"), -1, "<strong> still exists"); resetContent(); } function testPageModRemoveAttribute() { - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod remove attribute", - emptyParameters: [" <searchAttributes>", " <searchElements>", " [root]", " [ignoreCase]"], - status: "ERROR" + helpers.setInput('pagemod remove attribute '); + helpers.check({ + input: 'pagemod remove attribute ', + hints: '<searchAttributes> <searchElements> [root] [ignoreCase]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'ERROR', + args: { + searchAttributes: { value: undefined, status: 'INCOMPLETE' }, + searchElements: { value: undefined, status: 'INCOMPLETE' }, + root: { value: undefined }, + ignoreCase: { value: false }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pagemod remove attribute foo bar", - emptyParameters: [" [root]", " [ignoreCase]"], - status: "VALID" + helpers.setInput('pagemod remove attribute foo bar'); + helpers.check({ + input: 'pagemod remove attribute foo bar', + hints: ' [root] [ignoreCase]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + searchAttributes: { value: 'foo' }, + searchElements: { value: 'bar' }, + root: { value: undefined }, + ignoreCase: { value: false }, + } }); DeveloperToolbarTest.exec({ typed: "pagemod remove attribute foo bar", outputMatch: /^[^:]+: 0\. [^:]+: 0\.\s*$/ }); is(getContent(), initialHtml, "nothing changed in the page");
--- a/browser/devtools/commandline/test/browser_cmd_pref.js +++ b/browser/devtools/commandline/test/browser_cmd_pref.js @@ -64,102 +64,113 @@ function shutdown() { tiltEnabledOrig = undefined; tabSizeOrig = undefined; remoteHostOrig = undefined; imports = undefined; } function testPrefStatus() { - DeveloperToolbarTest.checkInputStatus({ - typed: "pref s", - markup: "IIIIVI", - status: "ERROR", - directTabText: "et" + helpers.setInput('pref'); + helpers.check({ + input: 'pref', + hints: '', + markup: 'IIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show", - markup: "VVVVVVVVV", - status: "ERROR", - emptyParameters: [ " <setting>" ] + helpers.setInput('pref s'); + helpers.check({ + input: 'pref s', + hints: 'et', + markup: 'IIIIVI', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show tempTBo", - markup: "VVVVVVVVVVEEEEEEE", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref sh'); + helpers.check({ + input: 'pref sh', + hints: 'ow', + markup: 'IIIIVII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show devtools.toolbar.ena", - markup: "VVVVVVVVVVIIIIIIIIIIIIIIIIIIII", - directTabText: "bled", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref show '); + helpers.check({ + input: 'pref show ', + markup: 'VVVVVVVVVV', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show hideIntro", - markup: "VVVVVVVVVVIIIIIIIII", - directTabText: "", - arrowTabText: "devtools.gcli.hideIntro", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref show usetexttospeech'); + helpers.check({ + input: 'pref show usetexttospeech', + hints: ' -> accessibility.usetexttospeech', + markup: 'VVVVVVVVVVIIIIIIIIIIIIIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show devtools.toolbar.enabled", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('pref show devtools.til'); + helpers.check({ + input: 'pref show devtools.til', + hints: 't.enabled', + markup: 'VVVVVVVVVVIIIIIIIIIIII', + status: 'ERROR', + tooltipState: 'true:importantFieldFlag', + args: { + setting: { value: undefined, status: 'INCOMPLETE' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show devtools.tilt.enabled 4", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE", - directTabText: "", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref reset devtools.tilt.enabled'); + helpers.check({ + input: 'pref reset devtools.tilt.enabled', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref show devtools.tilt.enabled", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('pref show devtools.tilt.enabled 4'); + helpers.check({ + input: 'pref show devtools.tilt.enabled 4', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref reset devtools.tilt.enabled", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", - status: "VALID", - emptyParameters: [ ] + helpers.setInput('pref set devtools.tilt.enabled 4'); + helpers.check({ + input: 'pref set devtools.tilt.enabled 4', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE', + status: 'ERROR', + args: { + setting: { arg: ' devtools.tilt.enabled' }, + value: { status: 'ERROR', message: 'Can\'t use \'4\'.' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref set devtools.tilt.enabled 4", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref set devtools.editor.tabsize 4'); + helpers.check({ + input: 'pref set devtools.editor.tabsize 4', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + setting: { arg: ' devtools.editor.tabsize' }, + value: { value: 4 }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "pref set devtools.editor.tabsize 4", - markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", - status: "VALID", - emptyParameters: [ ] - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "pref list", - markup: "EEEEVEEEE", - status: "ERROR", - emptyParameters: [ ] + helpers.setInput('pref list'); + helpers.check({ + input: 'pref list', + hints: '', + markup: 'EEEEVEEEE', + status: 'ERROR' }); } function testPrefSetEnable() { DeveloperToolbarTest.exec({ typed: "pref set devtools.editor.tabsize 9", args: { setting: imports.settings.getSetting("devtools.editor.tabsize"),
--- a/browser/devtools/commandline/test/browser_cmd_restart.js +++ b/browser/devtools/commandline/test/browser_cmd_restart.js @@ -5,41 +5,28 @@ const TEST_URI = "data:text/html;charset=utf-8,gcli-command-restart"; function test() { DeveloperToolbarTest.test(TEST_URI, [ testRestart ]); } function testRestart() { - DeveloperToolbarTest.checkInputStatus({ - typed: "restart", - markup: "VVVVVVV", - status: "VALID", - emptyParameters: [ " [nocache]" ], - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "restart ", - markup: "VVVVVVVV", - status: "VALID", - directTabText: "false" + helpers.setInput('restart'); + helpers.check({ + input: 'restart', + markup: 'VVVVVVV', + status: 'VALID', + args: { + nocache: { value: false }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "restart t", - markup: "VVVVVVVVI", - status: "ERROR", - directTabText: "rue" - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "restart --nocache", - markup: "VVVVVVVVVVVVVVVVV", - status: "VALID" - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "restart --noca", - markup: "VVVVVVVVEEEEEE", - status: "ERROR", + helpers.setInput('restart --nocache'); + helpers.check({ + input: 'restart --nocache', + markup: 'VVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + nocache: { value: true }, + } }); }
--- a/browser/devtools/commandline/test/browser_dbg_cmd_break.js +++ b/browser/devtools/commandline/test/browser_dbg_cmd_break.js @@ -6,82 +6,103 @@ const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" + "test/browser_dbg_cmd_break.html"; function test() { DeveloperToolbarTest.test(TEST_URI, [ testBreakCommands ]); } function testBreakCommands() { - - info('###################################################'); - info('###################################################'); - info('###################################################'); - info('###################################################'); - info('###################################################'); - info('###################################################'); - info(content.document.documentElement.innerHTML + '\n'); - - DeveloperToolbarTest.checkInputStatus({ - typed: "brea", - directTabText: "k", - status: "ERROR" + helpers.setInput('break'); + helpers.check({ + input: 'break', + hints: '', + markup: 'IIIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "break", - status: "ERROR" + helpers.setInput('break add'); + helpers.check({ + input: 'break add', + hints: '', + markup: 'IIIIIVIII', + status: 'ERROR' }); - DeveloperToolbarTest.checkInputStatus({ - typed: "break add", - status: "ERROR" - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "break add line", - emptyParameters: [ " <file>", " <line>" ], - status: "ERROR" + helpers.setInput('break add line'); + helpers.check({ + input: 'break add line', + hints: ' <file> <line>', + markup: 'VVVVVVVVVVVVVV', + status: 'ERROR' }); let pane = DebuggerUI.toggleDebugger(); var dbgConnected = DeveloperToolbarTest.checkCalled(function() { pane._frame.removeEventListener("Debugger:Connecting", dbgConnected, true); // Wait for the initial resume. let client = pane.contentWindow.gClient; var resumed = DeveloperToolbarTest.checkCalled(function() { var framesAdded = DeveloperToolbarTest.checkCalled(function() { - DeveloperToolbarTest.checkInputStatus({ - typed: "break add line " + TEST_URI + " " + content.wrappedJSObject.line0, - status: "VALID" + helpers.setInput('break add line ' + TEST_URI + ' ' + content.wrappedJSObject.line0); + helpers.check({ + hints: '', + status: 'VALID', + args: { + file: { value: TEST_URI }, + line: { value: content.wrappedJSObject.line0 }, + } }); + DeveloperToolbarTest.exec({ args: { type: 'line', file: TEST_URI, line: content.wrappedJSObject.line0 }, completed: false }); - DeveloperToolbarTest.checkInputStatus({ - typed: "break list", - status: "VALID" + helpers.setInput('break list'); + helpers.check({ + input: 'break list', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); var cleanup = DeveloperToolbarTest.checkCalled(function() { - DeveloperToolbarTest.checkInputStatus({ - typed: "break del 0", - status: "VALID" + helpers.setInput('break del 9'); + helpers.check({ + input: 'break del 9', + hints: '', + markup: 'VVVVVVVVVVE', + status: 'ERROR', + args: { + breakid: { status: 'ERROR', message: '9 is greater than maximum allowed: 0.' }, + } }); + + helpers.setInput('break del 0'); + helpers.check({ + input: 'break del 0', + hints: '', + markup: 'VVVVVVVVVVV', + status: 'VALID', + args: { + breakid: { value: 0 }, + } + }); + DeveloperToolbarTest.exec({ args: { breakid: 0 }, completed: false }); }); client.activeThread.resume(cleanup); });
--- a/browser/devtools/commandline/test/browser_gcli_web.js +++ b/browser/devtools/commandline/test/browser_gcli_web.js @@ -148,28 +148,31 @@ define('gclitest/index', ['require', 'ex options = options || {}; if (options.settings != null) { settings.setDefaults(options.settings); } window.display = new Display(options); var requisition = window.display.requisition; - exports.run({ - window: window, - display: window.display, - hideExec: true - }); - - window.testCommands = function() { - require([ 'gclitest/mockCommands' ], function(mockCommands) { - mockCommands.setup(); + // setTimeout keeps stack traces clear of RequireJS frames + window.setTimeout(function() { + exports.run({ + window: window, + display: window.display, + hideExec: true }); - }; - window.testCommands(); + + window.testCommands = function() { + require([ 'gclitest/mockCommands' ], function(mockCommands) { + mockCommands.setup(); + }); + }; + window.testCommands(); + }, 10); return { /** * The exact shape of the object returned by exec is likely to change in * the near future. If you do use it, please expect your code to break. */ exec: requisition.exec.bind(requisition), update: requisition.update.bind(requisition), @@ -866,84 +869,71 @@ exports.shutdown = function(opts) { * * helpers.status({ * // Test inputs * typed: "ech", // Required * cursor: 3, // Optional cursor position * * // Thing to check * status: "INCOMPLETE", // One of "VALID", "ERROR", "INCOMPLETE" - * emptyParameters: [ "<message>" ], // Still to type - * directTabText: "o", // Simple completion text - * arrowTabText: "", // When the completion is not an extension + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow * markup: "VVVIIIEEE", // What state should the error markup be in * }); */ exports.status = function(options, checks) { var requisition = options.display.requisition; var inputter = options.display.inputter; var completer = options.display.completer; - if (checks.typed) { + if (checks.typed != null) { inputter.setInput(checks.typed); } else { test.ok(false, "Missing typed for " + JSON.stringify(checks)); return; } - if (checks.cursor) { + if (checks.cursor != null) { inputter.setCursor(checks.cursor); } - if (checks.status) { + if (checks.status != null) { test.is(requisition.getStatus().toString(), checks.status, "status for " + checks.typed); } - var data = completer._getCompleterTemplateData(); - if (checks.emptyParameters != null) { - var realParams = data.emptyParameters; - test.is(realParams.length, - checks.emptyParameters.length, - 'emptyParameters.length for \'' + checks.typed + '\''); - - if (realParams.length === checks.emptyParameters.length) { - for (var i = 0; i < realParams.length; i++) { - test.is(realParams[i].replace(/\u00a0/g, ' '), - checks.emptyParameters[i], - 'emptyParameters[' + i + '] for \'' + checks.typed + '\''); - } - } + var actual = completer._getCompleterTemplateData(); + + if (checks.hints != null) { + var actualHints = actual.directTabText + + actual.emptyParameters.join('') + + actual.arrowTabText; + actualHints = actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + test.is(actualHints, + checks.hints, + 'hints'); } - if (checks.markup) { + if (checks.markup != null) { var cursor = checks.cursor ? checks.cursor.start : checks.typed.length; var statusMarkup = requisition.getInputStatusMarkup(cursor); var actualMarkup = statusMarkup.map(function(s) { return Array(s.string.length + 1).join(s.status.toString()[0]); }).join(''); test.is(checks.markup, actualMarkup, 'markup for ' + checks.typed); } - - if (checks.directTabText) { - test.is(data.directTabText, - checks.directTabText, - 'directTabText for \'' + checks.typed + '\''); - } - - if (checks.arrowTabText) { - test.is(' \u00a0\u21E5 ' + checks.arrowTabText, - data.arrowTabText, - 'arrowTabText for \'' + checks.typed + '\''); - } }; /** * We're splitting status into setup() which alters the state of the system * and check() which ensures that things are in the right place afterwards. */ exports.setInput = function(typed, cursor) { cachedOptions.display.inputter.setInput(typed); @@ -970,140 +960,126 @@ exports.pressTab = function() { /** * check() is the new status. Similar API except that it doesn't attempt to * alter the display/requisition at all, and it makes extra checks. * Available checks: * input: The text displayed in the input field * cursor: The position of the start of the cursor * status: One of "VALID", "ERROR", "INCOMPLETE" - * emptyParameters: Array of parameters still to type. e.g. [ "<message>" ] - * directTabText: Simple completion text - * arrowTabText: When the completion is not an extension (without arrow) + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow * markup: What state should the error markup be in. e.g. "VVVIIIEEE" * args: Maps of checks to make against the arguments: * value: i.e. assignment.value (which ignores defaultValue) * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned * Care should be taken with this since it's something of an * implementation detail * arg: The toString value of the argument * status: i.e. assignment.getStatus * message: i.e. assignment.getMessage * name: For commands - checks assignment.value.name */ exports.check = function(checks) { var requisition = cachedOptions.display.requisition; var completer = cachedOptions.display.completer; var actual = completer._getCompleterTemplateData(); - if (checks.input) { + if (checks.input != null) { test.is(cachedOptions.display.inputter.element.value, checks.input, 'input'); } - if (checks.cursor) { + if (checks.cursor != null) { test.is(cachedOptions.display.inputter.element.selectionStart, checks.cursor, 'cursor'); } - if (checks.status) { + if (checks.status != null) { test.is(requisition.getStatus().toString(), checks.status, 'status'); } - if (checks.markup) { + if (checks.markup != null) { var cursor = cachedOptions.display.inputter.element.selectionStart; var statusMarkup = requisition.getInputStatusMarkup(cursor); var actualMarkup = statusMarkup.map(function(s) { return Array(s.string.length + 1).join(s.status.toString()[0]); }).join(''); test.is(checks.markup, actualMarkup, 'markup'); } - if (checks.emptyParameters) { - var actualParams = actual.emptyParameters; - test.is(actualParams.length, - checks.emptyParameters.length, - 'emptyParameters.length'); - - if (actualParams.length === checks.emptyParameters.length) { - for (var i = 0; i < actualParams.length; i++) { - test.is(actualParams[i].replace(/\u00a0/g, ' '), - checks.emptyParameters[i], - 'emptyParameters[' + i + ']'); - } - } + if (checks.hints != null) { + var actualHints = actual.directTabText + + actual.emptyParameters.join('') + + actual.arrowTabText; + actualHints = actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + test.is(actualHints, + checks.hints, + 'hints'); } - if (checks.directTabText) { - test.is(actual.directTabText, - checks.directTabText, - 'directTabText'); - } - - if (checks.arrowTabText) { - test.is(actual.arrowTabText, - ' \u00a0\u21E5 ' + checks.arrowTabText, - 'arrowTabText'); - } - - if (checks.args) { + if (checks.args != null) { Object.keys(checks.args).forEach(function(paramName) { var check = checks.args[paramName]; var assignment; if (paramName === 'command') { assignment = requisition.commandAssignment; } else { assignment = requisition.getAssignment(paramName); } if (assignment == null) { test.ok(false, 'Unknown arg: ' + paramName); return; } - if (check.value) { + if (check.value != null) { test.is(assignment.value, check.value, 'arg[\'' + paramName + '\'].value'); } - if (check.name) { + if (check.name != null) { test.is(assignment.value.name, check.name, 'arg[\'' + paramName + '\'].name'); } - if (check.type) { + if (check.type != null) { test.is(assignment.arg.type, check.type, 'arg[\'' + paramName + '\'].type'); } - if (check.arg) { + if (check.arg != null) { test.is(assignment.arg.toString(), check.arg, 'arg[\'' + paramName + '\'].arg'); } - if (check.status) { + if (check.status != null) { test.is(assignment.getStatus().toString(), check.status, 'arg[\'' + paramName + '\'].status'); } - if (check.message) { + if (check.message != null) { test.is(assignment.getMessage(), check.message, 'arg[\'' + paramName + '\'].message'); } }); } }; @@ -2055,75 +2031,65 @@ exports.tscook = { }; exports.tslong = { name: 'tslong', description: 'long param tests to catch problems with the jsb command', returnValue:'string', params: [ { - name: 'url', + name: 'msg', type: 'string', - description: 'tslongUrlDesc' + description: 'msg Desc' }, { - group: "tslongOptionsDesc", + group: "Options Desc", params: [ { - name: 'indentSize', + name: 'num', type: 'number', - description: 'tslongIndentSizeDesc', + description: 'num Desc', defaultValue: 2 }, { - name: 'indentChar', + name: 'sel', type: { name: 'selection', lookup: [ { name: "space", value: " " }, { name: "tab", value: "\t" } ] }, - description: 'tslongIndentCharDesc', + description: 'sel Desc', defaultValue: ' ', }, { - name: 'preserveNewlines', + name: 'bool', type: 'boolean', - description: 'tslongPreserveNewlinesDesc' + description: 'bool Desc' }, { - name: 'preserveMaxNewlines', + name: 'num2', type: 'number', - description: 'tslongPreserveMaxNewlinesDesc', + description: 'num2 Desc', defaultValue: -1 }, { - name: 'jslintHappy', + name: 'bool2', type: 'boolean', - description: 'tslongJslintHappyDesc' + description: 'bool2 Desc' }, { - name: 'braceStyle', + name: 'sel2', type: { name: 'selection', data: ['collapse', 'expand', 'end-expand', 'expand-strict'] }, - description: 'tslongBraceStyleDesc', + description: 'sel2 Desc', defaultValue: "collapse" - }, - { - name: 'noSpaceBeforeConditional', - type: 'boolean', - description: 'tslongNoSpaceBeforeConditionalDesc' - }, - { - name: 'unescapeStrings', - type: 'boolean', - description: 'tslongUnescapeStringsDesc' } ] } ], exec: createExec('tslong') }; @@ -2165,288 +2131,338 @@ exports.shutdown = function(options) { exports.testActivate = function(options) { if (!options.display) { test.log('No display. Skipping activate tests'); return; } helpers.setInput(''); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput(' '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('tsr'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <text>' ] + hints: ' <text>' }); helpers.setInput('tsr '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '<text>' ] + hints: '<text>' }); helpers.setInput('tsr b'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('tsb'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [toggle]' ] + hints: ' [toggle]' }); helpers.setInput('tsm'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <abc>', ' <txt>', ' <num>' ] + hints: ' <abc> <txt> <num>' }); helpers.setInput('tsm '); helpers.check({ - emptyParameters: [ ' <txt>', ' <num>' ], - arrowTabText: '', - directTabText: 'a' + hints: 'a <txt> <num>' }); helpers.setInput('tsm a'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <txt>', ' <num>' ] + hints: ' <txt> <num>' }); helpers.setInput('tsm a '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '<txt>', ' <num>' ] + hints: '<txt> <num>' }); helpers.setInput('tsm a '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '<txt>', ' <num>' ] + hints: '<txt> <num>' }); helpers.setInput('tsm a d'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <num>' ] + hints: ' <num>' }); helpers.setInput('tsm a "d d"'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <num>' ] + hints: ' <num>' }); helpers.setInput('tsm a "d '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <num>' ] + hints: ' <num>' }); helpers.setInput('tsm a "d d" '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '<num>' ] + hints: '<num>' }); helpers.setInput('tsm a "d d '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <num>' ] + hints: ' <num>' }); helpers.setInput('tsm d r'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <num>' ] + hints: ' <num>' }); helpers.setInput('tsm a d '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '<num>' ] + hints: '<num>' }); helpers.setInput('tsm a d 4'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('tsg'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' <solo>', ' [options]' ] + hints: ' <solo> [options]' }); helpers.setInput('tsg '); helpers.check({ - emptyParameters: [ ' [options]' ], - arrowTabText: '', - directTabText: 'aaa' + hints: 'aaa [options]' }); helpers.setInput('tsg a'); helpers.check({ - emptyParameters: [ ' [options]' ], - arrowTabText: '', - directTabText: 'aa' + hints: 'aa [options]' }); helpers.setInput('tsg b'); helpers.check({ - emptyParameters: [ ' [options]' ], - arrowTabText: '', - directTabText: 'bb' + hints: 'bb [options]' }); helpers.setInput('tsg d'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aa'); helpers.check({ - emptyParameters: [ ' [options]' ], - arrowTabText: '', - directTabText: 'a' + hints: 'a [options]' }); helpers.setInput('tsg aaa'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aaa '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '[options]' ] + hints: '[options]' }); helpers.setInput('tsg aaa d'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aaa dddddd'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aaa dddddd '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ '[options]' ] + hints: '[options]' }); helpers.setInput('tsg aaa "d'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aaa "d d'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsg aaa "d d"'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [ ' [options]' ] + hints: ' [options]' }); helpers.setInput('tsn ex '); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('selarr'); helpers.check({ - directTabText: '', - emptyParameters: [], - arrowTabText: 'tselarr' + hints: ' -> tselarr' }); helpers.setInput('tselar 1'); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('tselar 1', 7); helpers.check({ - directTabText: '', - arrowTabText: '', - emptyParameters: [] + hints: '' }); helpers.setInput('tselar 1', 6); helpers.check({ - directTabText: '', - emptyParameters: [], - arrowTabText: 'tselarr' + hints: ' -> tselarr' }); helpers.setInput('tselar 1', 5); helpers.check({ - directTabText: '', - emptyParameters: [], - arrowTabText: 'tselarr' + hints: ' -> tselarr' + }); +}; + +exports.testLong = function(options) { + helpers.setInput('tslong --sel'); + helpers.check({ + input: 'tslong --sel', + hints: ' <selection> <msg> [options]', + markup: 'VVVVVVVIIIII' + }); + + helpers.pressTab(); + helpers.check({ + input: 'tslong --sel ', + hints: 'space <msg> [options]', + markup: 'VVVVVVVIIIIIV' + }); + + helpers.setInput('tslong --sel '); + helpers.check({ + input: 'tslong --sel ', + hints: 'space <msg> [options]', + markup: 'VVVVVVVIIIIIV' + }); + + helpers.setInput('tslong --sel s'); + helpers.check({ + input: 'tslong --sel s', + hints: 'pace <msg> [options]', + markup: 'VVVVVVVIIIIIVI' + }); + + helpers.setInput('tslong --num '); + helpers.check({ + input: 'tslong --num ', + hints: '<number> <msg> [options]', + markup: 'VVVVVVVIIIIIV' + }); + + helpers.setInput('tslong --num 42'); + helpers.check({ + input: 'tslong --num 42', + hints: ' <msg> [options]', + markup: 'VVVVVVVVVVVVVVV' + }); + + helpers.setInput('tslong --num 42 '); + helpers.check({ + input: 'tslong --num 42 ', + hints: '<msg> [options]', + markup: 'VVVVVVVVVVVVVVVV' + }); + + helpers.setInput('tslong --num 42 --se'); + helpers.check({ + input: 'tslong --num 42 --se', + hints: 'l <msg> [options]', + markup: 'VVVVVVVVVVVVVVVVIIII' }); -}; - + + helpers.pressTab(); + helpers.check({ + input: 'tslong --num 42 --sel ', + hints: 'space <msg> [options]', + markup: 'VVVVVVVVVVVVVVVVIIIIIV' + }); + + helpers.pressTab(); + helpers.check({ + input: 'tslong --num 42 --sel space ', + hints: '<msg> [options]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV' + }); + + helpers.setInput('tslong --num 42 --sel '); + helpers.check({ + input: 'tslong --num 42 --sel ', + hints: 'space <msg> [options]', + markup: 'VVVVVVVVVVVVVVVVIIIIIV' + }); + + helpers.setInput('tslong --num 42 --sel space '); + helpers.check({ + input: 'tslong --num 42 --sel space ', + hints: '<msg> [options]', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV' + }); +}; + +exports.testNoTab = function(options) { + helpers.setInput('tss'); + helpers.pressTab(); + helpers.check({ + input: 'tss ', + markup: 'VVVV', + hints: '' + }); + + helpers.pressTab(); + helpers.check({ + input: 'tss ', + markup: 'VVVV', + hints: '' + }); + + helpers.setInput('xxxx'); + helpers.check({ + input: 'xxxx', + markup: 'EEEE', + hints: '' + }); + + helpers.pressTab(); + helpers.check({ + input: 'xxxx', + markup: 'EEEE', + hints: '' + }); +}; + +exports.testOutstanding = function(options) { + // See bug 779800 + /* + helpers.setInput('tsg --txt1 ddd '); + helpers.check({ + input: 'tsg --txt1 ddd ', + hints: 'aaa [options]', + markup: 'VVVVVVVVVVVVVVV' + }); + */ +}; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -2630,33 +2646,33 @@ var mockDoc = { define('gclitest/testHelp', ['require', 'exports', 'module' , 'gclitest/helpers'], function(require, exports, module) { var helpers = require('gclitest/helpers'); exports.testHelpStatus = function(options) { helpers.status(options, { typed: 'help', + hints: ' [search]', markup: 'VVVV', - status: 'VALID', - emptyParameters: [ " [search]" ] + status: 'VALID' }); helpers.status(options, { typed: 'help foo', markup: 'VVVVVVVV', status: 'VALID', - emptyParameters: [ ] + hints: '' }); helpers.status(options, { typed: 'help foo bar', markup: 'VVVVVVVVVVVV', status: 'VALID', - emptyParameters: [ ] + hints: '' }); }; exports.testHelpExec = function(options) { if (options.isFirefox) { helpers.exec(options, { typed: 'help', args: { search: null }, @@ -2947,144 +2963,128 @@ exports.testCompleted = function(options num: { type: 'Argument' }, arr: { type: 'ArrayArgument' }, } }); helpers.setInput('tsn dif '); helpers.check({ input: 'tsn dif ', + hints: '<text>', markup: 'VVVVVVVV', cursor: 8, - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ '<text>' ], args: { command: { name: 'tsn dif', type: 'MergedArgument' }, text: { type: 'BlankArgument', status: 'INCOMPLETE' } } }); helpers.setInput('tsn di'); helpers.pressTab(); helpers.check({ input: 'tsn dif ', + hints: '<text>', markup: 'VVVVVVVV', cursor: 8, - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ '<text>' ], args: { command: { name: 'tsn dif', type: 'Argument' }, text: { type: 'Argument', status: 'INCOMPLETE' } } }); // The above 2 tests take different routes to 'tsn dif '. The results should // be similar. The difference is in args.command.type. helpers.setInput('tsg -'); helpers.check({ input: 'tsg -', + hints: '-txt1 <solo> [options]', markup: 'VVVVI', cursor: 5, - directTabText: '-txt1', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { solo: { value: undefined, status: 'INCOMPLETE' }, txt1: { value: undefined, status: 'VALID' }, bool: { value: undefined, status: 'VALID' }, txt2: { value: undefined, status: 'VALID' }, num: { value: undefined, status: 'VALID' } } }); helpers.pressTab(); helpers.check({ input: 'tsg --txt1 ', + hints: '<string> <solo> [options]', markup: 'VVVVIIIIIIV', cursor: 11, - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ '[options]' ], // Bug 770830: '<txt1>', ' <solo>' args: { solo: { value: undefined, status: 'INCOMPLETE' }, txt1: { value: undefined, status: 'INCOMPLETE' }, bool: { value: undefined, status: 'VALID' }, txt2: { value: undefined, status: 'VALID' }, num: { value: undefined, status: 'VALID' } } }); helpers.setInput('tsg --txt1 fred'); helpers.check({ input: 'tsg --txt1 fred', + hints: ' <solo> [options]', markup: 'VVVVVVVVVVVVVVV', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], // Bug 770830: ' <solo>' args: { solo: { value: undefined, status: 'INCOMPLETE' }, txt1: { value: 'fred', status: 'VALID' }, bool: { value: undefined, status: 'VALID' }, txt2: { value: undefined, status: 'VALID' }, num: { value: undefined, status: 'VALID' } } }); helpers.setInput('tscook key value --path path --'); helpers.check({ input: 'tscook key value --path path --', + hints: 'domain [options]', markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVII', - directTabText: 'domain', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { key: { value: 'key', status: 'VALID' }, value: { value: 'value', status: 'VALID' }, path: { value: 'path', status: 'VALID' }, domain: { value: undefined, status: 'VALID' }, secure: { value: false, status: 'VALID' } } }); helpers.setInput('tscook key value --path path --domain domain --'); helpers.check({ input: 'tscook key value --path path --domain domain --', + hints: 'secure [options]', markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVII', - directTabText: 'secure', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { key: { value: 'key', status: 'VALID' }, value: { value: 'value', status: 'VALID' }, path: { value: 'path', status: 'VALID' }, domain: { value: 'domain', status: 'VALID' }, secure: { value: false, status: 'VALID' } } }); }; exports.testCase = function(options) { helpers.setInput('tsg AA'); helpers.check({ input: 'tsg AA', + hints: ' [options] -> aaa', markup: 'VVVVII', - directTabText: '', - arrowTabText: 'aaa', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { solo: { value: undefined, text: 'AA', status: 'INCOMPLETE' }, txt1: { value: undefined, status: 'VALID' }, bool: { value: undefined, status: 'VALID' }, txt2: { value: undefined, status: 'VALID' }, num: { value: undefined, status: 'VALID' } } }); @@ -3119,135 +3119,117 @@ exports.testIncomplete = function(option 'unassigned.isIncompleteName: tsg -'); }; exports.testHidden = function(options) { helpers.setInput('tshidde'); helpers.check({ input: 'tshidde', markup: 'EEEEEEE', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ], + hints: '', }); helpers.setInput('tshidden'); helpers.check({ input: 'tshidden', + hints: ' [options]', markup: 'VVVVVVVV', - directTabText: '', - arrowTabText: '', status: 'VALID', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --vis'); helpers.check({ input: 'tshidden --vis', + hints: 'ible [options]', markup: 'VVVVVVVVVIIIII', - directTabText: 'ible', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --invisiblestrin'); helpers.check({ input: 'tshidden --invisiblestrin', + hints: ' [options]', markup: 'VVVVVVVVVEEEEEEEEEEEEEEEE', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --invisiblestring'); helpers.check({ input: 'tshidden --invisiblestring', + hints: ' <string> [options]', markup: 'VVVVVVVVVIIIIIIIIIIIIIIIII', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'INCOMPLETE' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --invisiblestring x'); helpers.check({ input: 'tshidden --invisiblestring x', + hints: ' [options]', markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV', - directTabText: '', - arrowTabText: '', status: 'VALID', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: 'x', status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --invisibleboolea'); helpers.check({ input: 'tshidden --invisibleboolea', + hints: ' [options]', markup: 'VVVVVVVVVEEEEEEEEEEEEEEEEE', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); helpers.setInput('tshidden --invisibleboolean'); helpers.check({ input: 'tshidden --invisibleboolean', + hints: ' [options]', markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVV', - directTabText: '', - arrowTabText: '', status: 'VALID', - emptyParameters: [ ' [options]' ], args: { visible: { value: undefined, status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: true, status: 'VALID' } } }); helpers.setInput('tshidden --visible xxx'); helpers.check({ input: 'tshidden --visible xxx', markup: 'VVVVVVVVVVVVVVVVVVVVVV', - directTabText: '', - arrowTabText: '', status: 'VALID', - emptyParameters: [ ], + hints: '', args: { visible: { value: 'xxx', status: 'VALID' }, invisiblestring: { value: undefined, status: 'VALID' }, invisibleboolean: { value: undefined, status: 'VALID' } } }); }; @@ -3278,24 +3260,24 @@ define('gclitest/testIntro', ['require', test.log('Skipping testIntroStatus in Firefox.'); return; } helpers.status(options, { typed: 'intro', markup: 'VVVVV', status: 'VALID', - emptyParameters: [ ] + hints: '' }); helpers.status(options, { typed: 'intro foo', markup: 'VVVVVVEEE', status: 'ERROR', - emptyParameters: [ ] + hints: '' }); }; exports.testIntroExec = function(options) { if (options.isFirefox) { test.log('Skipping testIntroExec in Firefox.'); return; } @@ -3565,21 +3547,21 @@ function check(initial, action, after, c } var assignment = requisition.getAssignmentAt(cursor); switch (action) { case COMPLETES_TO: requisition.complete({ start: cursor, end: cursor }, choice); break; case KEY_UPS_TO: - assignment.increment(); + requisition.increment(assignment); break; case KEY_DOWNS_TO: - assignment.decrement(); + requisition.decrement(assignment); break; } test.is(after, requisition.toString(), initial + ' + ' + action + ' -> ' + after); if (expectedCursor != null) { if (inputter) { @@ -3712,30 +3694,26 @@ exports.shutdown = function(options) { helpers.shutdown(options); }; exports.testOptions = function(options) { helpers.setInput('tslong'); helpers.check({ input: 'tslong', markup: 'VVVVVV', - directTabText: '', - arrowTabText: '', status: 'ERROR', - emptyParameters: [ ' <url>', ' [options]' ], + hints: ' <msg> [options]', args: { - url: { value: undefined, status: 'INCOMPLETE' }, - indentSize: { value: undefined, status: 'VALID' }, - indentChar: { value: undefined, status: 'VALID' }, - preserveNewlines: { value: undefined, status: 'VALID' }, - preserveMaxNewlines: { value: undefined, status: 'VALID' }, - jslintHappy: { value: undefined, status: 'VALID' }, - braceStyle: { value: undefined, status: 'VALID' }, - noSpaceBeforeConditional: { value: undefined, status: 'VALID' }, - unescapeStrings: { value: undefined, status: 'VALID' } + msg: { value: undefined, status: 'INCOMPLETE' }, + num: { value: undefined, status: 'VALID' }, + sel: { value: undefined, status: 'VALID' }, + bool: { value: undefined, status: 'VALID' }, + bool2: { value: undefined, status: 'VALID' }, + sel2: { value: undefined, status: 'VALID' }, + num2: { value: undefined, status: 'VALID' } } }); }; }); /* @@ -3781,123 +3759,116 @@ exports.shutdown = function(options) { exports.testPrefShowStatus = function(options) { if (options.isFirefox) { test.log('Skipping testPrefShowStatus in Firefox.'); return; } helpers.status(options, { typed: 'pref s', + hints: 'et', markup: 'IIIIVI', - status: 'ERROR', - directTabText: 'et' + status: 'ERROR' }); helpers.status(options, { typed: 'pref show', + hints: ' <setting>', markup: 'VVVVVVVVV', - status: 'ERROR', - emptyParameters: [ ' <setting>' ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref show ', + hints: 'allowSet', markup: 'VVVVVVVVVV', - status: 'ERROR', - emptyParameters: [ ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref show tempTBo', + hints: 'ol', markup: 'VVVVVVVVVVIIIIIII', - directTabText: 'ol', - status: 'ERROR', - emptyParameters: [ ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref show tempTBool', markup: 'VVVVVVVVVVVVVVVVVVV', - directTabText: '', status: 'VALID', - emptyParameters: [ ] + hints: '' }); helpers.status(options, { typed: 'pref show tempTBool 4', markup: 'VVVVVVVVVVVVVVVVVVVVE', - directTabText: '', status: 'ERROR', - emptyParameters: [ ] + hints: '' }); helpers.status(options, { typed: 'pref show tempNumber 4', markup: 'VVVVVVVVVVVVVVVVVVVVVE', - directTabText: '', status: 'ERROR', - emptyParameters: [ ] + hints: '' }); }; exports.testPrefSetStatus = function(options) { if (options.isFirefox) { test.log('Skipping testPrefSetStatus in Firefox.'); return; } helpers.status(options, { typed: 'pref s', + hints: 'et', markup: 'IIIIVI', status: 'ERROR', - directTabText: 'et' }); helpers.status(options, { typed: 'pref set', + hints: ' <setting> <value>', markup: 'VVVVVVVV', - status: 'ERROR', - emptyParameters: [ ' <setting>', ' <value>' ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref xxx', markup: 'EEEEVEEE', status: 'ERROR' }); helpers.status(options, { typed: 'pref set ', + hints: 'allowSet <value>', markup: 'VVVVVVVVV', - status: 'ERROR', - emptyParameters: [ ' <value>' ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref set tempTBo', + hints: 'ol <value>', markup: 'VVVVVVVVVIIIIIII', - directTabText: 'ol', - status: 'ERROR', - emptyParameters: [ ' <value>' ] + status: 'ERROR' }); helpers.status(options, { typed: 'pref set tempTBool 4', markup: 'VVVVVVVVVVVVVVVVVVVE', - directTabText: '', status: 'ERROR', - emptyParameters: [ ] + hints: '' }); helpers.status(options, { typed: 'pref set tempNumber 4', markup: 'VVVVVVVVVVVVVVVVVVVVV', - directTabText: '', status: 'VALID', - emptyParameters: [ ] + hints: '' }); }; exports.testPrefExec = function(options) { if (options.isFirefox) { test.log('Skipping testPrefExec in Firefox.'); return; }
--- a/browser/devtools/commandline/test/head.js +++ b/browser/devtools/commandline/test/head.js @@ -8,17 +8,17 @@ const TEST_BASE_HTTPS = "https://example let console = (function() { let tempScope = {}; Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); return tempScope.console; })(); // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); -Services.scriptloader.loadSubScript(testDir + "/helper.js", this); +Services.scriptloader.loadSubScript(testDir + "/helpers.js", this); /** * Open a new tab at a URL and call a callback on load */ function addTab(aURL, aCallback) { waitForExplicitFinish();
deleted file mode 100644 --- a/browser/devtools/commandline/test/helper.js +++ /dev/null @@ -1,459 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - - -/* - * - * DO NOT ALTER THIS FILE WITHOUT KEEPING IT IN SYNC WITH THE OTHER COPIES - * OF THIS FILE. - * - * UNAUTHORIZED ALTERATION WILL RESULT IN THE ALTEREE BEING SENT TO SIT ON - * THE NAUGHTY STEP. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * FOR A LONG TIME. - * - */ - - -/** - * Various functions for testing DeveloperToolbar. - * Parts of this code exist in: - * - browser/devtools/commandline/test/head.js - * - browser/devtools/shared/test/head.js - */ -let DeveloperToolbarTest = { }; - -/** - * Paranoid DeveloperToolbar.show(); - */ -DeveloperToolbarTest.show = function DTT_show(aCallback) { - if (DeveloperToolbar.visible) { - ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar"); - } - else { - DeveloperToolbar.show(true, aCallback); - } -}; - -/** - * Paranoid DeveloperToolbar.hide(); - */ -DeveloperToolbarTest.hide = function DTT_hide() { - if (!DeveloperToolbar.visible) { - ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar"); - } - else { - DeveloperToolbar.display.inputter.setInput(""); - DeveloperToolbar.hide(); - } -}; - -/** - * check() is the new status. Similar API except that it doesn't attempt to - * alter the display/requisition at all, and it makes extra checks. - * Test inputs - * typed: The text to type at the input - * Available checks: - * input: The text displayed in the input field - * cursor: The position of the start of the cursor - * status: One of "VALID", "ERROR", "INCOMPLETE" - * emptyParameters: Array of parameters still to type. e.g. [ "<message>" ] - * directTabText: Simple completion text - * arrowTabText: When the completion is not an extension (without arrow) - * markup: What state should the error markup be in. e.g. "VVVIIIEEE" - * args: Maps of checks to make against the arguments: - * value: i.e. assignment.value (which ignores defaultValue) - * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned - * Care should be taken with this since it's something of an - * implementation detail - * arg: The toString value of the argument - * status: i.e. assignment.getStatus - * message: i.e. assignment.getMessage - * name: For commands - checks assignment.value.name - */ -DeveloperToolbarTest.checkInputStatus = function DTT_checkInputStatus(checks) { - if (!checks.emptyParameters) { - checks.emptyParameters = []; - } - if (!checks.directTabText) { - checks.directTabText = ''; - } - if (!checks.arrowTabText) { - checks.arrowTabText = ''; - } - - var display = DeveloperToolbar.display; - - if (checks.typed) { - info('Starting tests for ' + checks.typed); - display.inputter.setInput(checks.typed); - } - else { - ok(false, "Missing typed for " + JSON.stringify(checks)); - return; - } - - if (checks.cursor) { - display.inputter.setCursor(checks.cursor) - } - - var cursor = checks.cursor ? checks.cursor.start : checks.typed.length; - - var requisition = display.requisition; - var completer = display.completer; - var actual = completer._getCompleterTemplateData(); - - /* - if (checks.input) { - is(display.inputter.element.value, - checks.input, - 'input'); - } - - if (checks.cursor) { - is(display.inputter.element.selectionStart, - checks.cursor, - 'cursor'); - } - */ - - if (checks.status) { - is(requisition.getStatus().toString(), - checks.status, - 'status'); - } - - if (checks.markup) { - var statusMarkup = requisition.getInputStatusMarkup(cursor); - var actualMarkup = statusMarkup.map(function(s) { - return Array(s.string.length + 1).join(s.status.toString()[0]); - }).join(''); - - is(checks.markup, - actualMarkup, - 'markup'); - } - - if (checks.emptyParameters) { - var actualParams = actual.emptyParameters; - is(actualParams.length, - checks.emptyParameters.length, - 'emptyParameters.length'); - - if (actualParams.length === checks.emptyParameters.length) { - for (var i = 0; i < actualParams.length; i++) { - is(actualParams[i].replace(/\u00a0/g, ' '), - checks.emptyParameters[i], - 'emptyParameters[' + i + ']'); - } - } - } - - if (checks.directTabText) { - is(actual.directTabText, - checks.directTabText, - 'directTabText'); - } - - if (checks.arrowTabText) { - is(actual.arrowTabText, - ' \u00a0\u21E5 ' + checks.arrowTabText, - 'arrowTabText'); - } - - if (checks.args) { - Object.keys(checks.args).forEach(function(paramName) { - var check = checks.args[paramName]; - - var assignment; - if (paramName === 'command') { - assignment = requisition.commandAssignment; - } - else { - assignment = requisition.getAssignment(paramName); - } - - if (assignment == null) { - ok(false, 'Unknown parameter: ' + paramName); - return; - } - - if (check.value) { - is(assignment.value, - check.value, - 'checkStatus value for ' + paramName); - } - - if (check.name) { - is(assignment.value.name, - check.name, - 'checkStatus name for ' + paramName); - } - - if (check.type) { - is(assignment.arg.type, - check.type, - 'checkStatus type for ' + paramName); - } - - if (check.arg) { - is(assignment.arg.toString(), - check.arg, - 'checkStatus arg for ' + paramName); - } - - if (check.status) { - is(assignment.getStatus().toString(), - check.status, - 'checkStatus status for ' + paramName); - } - - if (check.message) { - is(assignment.getMessage(), - check.message, - 'checkStatus message for ' + paramName); - } - }); - } -}; - -/** - * Execute a command: - * - * DeveloperToolbarTest.exec({ - * // Test inputs - * typed: "echo hi", // Optional, uses existing if undefined - * - * // Thing to check - * args: { message: "hi" }, // Check that the args were understood properly - * outputMatch: /^hi$/, // RegExp to test against textContent of output - * // (can also be array of RegExps) - * blankOutput: true, // Special checks when there is no output - * }); - */ -DeveloperToolbarTest.exec = function DTT_exec(tests) { - tests = tests || {}; - - if (tests.typed) { - DeveloperToolbar.display.inputter.setInput(tests.typed); - } - - let typed = DeveloperToolbar.display.inputter.getInputState().typed; - let output = DeveloperToolbar.display.requisition.exec(); - - is(typed, output.typed, 'output.command for: ' + typed); - - if (tests.completed !== false) { - ok(output.completed, 'output.completed false for: ' + typed); - } - else { - // It is actually an error if we say something is async and it turns - // out not to be? For now we're saying 'no' - // ok(!output.completed, 'output.completed true for: ' + typed); - } - - if (tests.args != null) { - is(Object.keys(tests.args).length, Object.keys(output.args).length, - 'arg count for ' + typed); - - Object.keys(output.args).forEach(function(arg) { - let expectedArg = tests.args[arg]; - let actualArg = output.args[arg]; - - if (typeof expectedArg === 'function') { - ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg); - } - else { - if (Array.isArray(expectedArg)) { - if (!Array.isArray(actualArg)) { - ok(false, 'actual is not an array. ' + typed + '/' + arg); - return; - } - - is(expectedArg.length, actualArg.length, - 'array length: ' + typed + '/' + arg); - for (let i = 0; i < expectedArg.length; i++) { - is(expectedArg[i], actualArg[i], - 'member: "' + typed + '/' + arg + '/' + i); - } - } - else { - is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); - } - } - }); - } - - let displayed = DeveloperToolbar.outputPanel._div.textContent; - - if (tests.outputMatch) { - var doTest = function(match, against) { - if (!match.test(against)) { - ok(false, "html output for " + typed + " against " + match.source + - " (textContent sent to info)"); - info("Actual textContent"); - info(against); - } - } - if (Array.isArray(tests.outputMatch)) { - tests.outputMatch.forEach(function(match) { - doTest(match, displayed); - }); - } - else { - doTest(tests.outputMatch, displayed); - } - } - - if (tests.blankOutput != null) { - if (!/^$/.test(displayed)) { - ok(false, "html output for " + typed + " (textContent sent to info)"); - info("Actual textContent"); - info(displayed); - } - } -}; - -/** - * Quick wrapper around the things you need to do to run DeveloperToolbar - * command tests: - * - Set the pref 'devtools.toolbar.enabled' to true - * - Add a tab pointing at |uri| - * - Open the DeveloperToolbar - * - Register a cleanup function to undo the above - * - Run the tests - * - * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...' - * @param target Either a function or array of functions containing the tests - * to run. If an array of test function is passed then we will clear up after - * the tests have completed. If a single test function is passed then this - * function should arrange for 'finish()' to be called on completion. - */ -DeveloperToolbarTest.test = function DTT_test(uri, target) { - let menuItem = document.getElementById("menu_devToolbar"); - let command = document.getElementById("Tools:DevToolbar"); - let appMenuItem = document.getElementById("appmenu_devToolbar"); - - registerCleanupFunction(function() { - DeveloperToolbarTest.hide(); - - // a.k.a Services.prefs.clearUserPref("devtools.toolbar.enabled"); - if (menuItem) { - menuItem.hidden = true; - } - if (command) { - command.setAttribute("disabled", "true"); - } - if (appMenuItem) { - appMenuItem.hidden = true; - } - - // leakHunt({ DeveloperToolbar: DeveloperToolbar }); - }); - - // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true); - if (menuItem) { - menuItem.hidden = false; - } - if (command) { - command.removeAttribute("disabled"); - } - if (appMenuItem) { - appMenuItem.hidden = false; - } - - waitForExplicitFinish(); - - gBrowser.selectedTab = gBrowser.addTab(); - content.location = uri; - - let tab = gBrowser.selectedTab; - let browser = gBrowser.getBrowserForTab(tab); - - var onTabLoad = function() { - browser.removeEventListener("load", onTabLoad, true); - - DeveloperToolbarTest.show(function() { - if (Array.isArray(target)) { - try { - target.forEach(function(func) { - func(browser, tab); - }) - } - finally { - DeveloperToolbarTest._checkFinish(); - } - } - else { - try { - target(browser, tab); - } - catch (ex) { - ok(false, "" + ex); - DeveloperToolbarTest._finish(); - throw ex; - } - } - }); - } - - browser.addEventListener("load", onTabLoad, true); -}; - -DeveloperToolbarTest._outstanding = []; - -DeveloperToolbarTest._checkFinish = function() { - if (DeveloperToolbarTest._outstanding.length == 0) { - DeveloperToolbarTest._finish(); - } -} - -DeveloperToolbarTest._finish = function() { - DeveloperToolbarTest.closeAllTabs(); - finish(); -} - -DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { - var todo = function() { - var reply = aFunc.apply(aScope, arguments); - DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { - return aJob != todo; - }); - DeveloperToolbarTest._checkFinish(); - return reply; - } - DeveloperToolbarTest._outstanding.push(todo); - return todo; -}; - -DeveloperToolbarTest.checkNotCalled = function(aMsg, aFunc, aScope) { - return function() { - ok(false, aMsg); - return aFunc.apply(aScope, arguments); - } -}; - -/** - * - */ -DeveloperToolbarTest.closeAllTabs = function() { - while (gBrowser.tabs.length > 1) { - gBrowser.removeCurrentTab(); - } -};
new file mode 100644 --- /dev/null +++ b/browser/devtools/commandline/test/helpers.js @@ -0,0 +1,881 @@ +/* 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/. */ + + +/* + * + * DO NOT ALTER THIS FILE WITHOUT KEEPING IT IN SYNC WITH THE OTHER COPIES + * OF THIS FILE. + * + * UNAUTHORIZED ALTERATION WILL RESULT IN THE ALTEREE BEING SENT TO SIT ON + * THE NAUGHTY STEP. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * FOR A LONG TIME. + * + */ + + +/* + * Use as a JSM + * ------------ + * helpers._createDebugCheck() and maybe other functions in this file can be + * useful at runtime, so it is possible to use helpers.js as a JSM. + * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to + * DeveloperToolbar.jsm the following: + * + * XPCOMUtils.defineLazyModuleGetter(this, "helpers", + * "resource:///modules/devtools/helpers.jsm"); + * + * At the bottom of DeveloperToolbar.prototype._onload add this: + * + * var options = { display: this.display }; + * this._input.onkeypress = function(ev) { + * helpers.setup(options); + * dump(helpers._createDebugCheck() + '\n\n'); + * }; + * + * Now GCLI will emit output on every keypress that both explains the state + * of GCLI and can be run as a test case. + */ + +var EXPORTED_SYMBOLS = [ 'helpers' ]; + +var test = { }; + +/** + * Various functions for testing DeveloperToolbar. + * Parts of this code exist in: + * - browser/devtools/commandline/test/head.js + * - browser/devtools/shared/test/head.js + */ +let DeveloperToolbarTest = { }; + +/** + * Paranoid DeveloperToolbar.show(); + */ +DeveloperToolbarTest.show = function DTT_show(aCallback) { + if (DeveloperToolbar.visible) { + ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar"); + } + else { + DeveloperToolbar.show(true, aCallback); + } +}; + +/** + * Paranoid DeveloperToolbar.hide(); + */ +DeveloperToolbarTest.hide = function DTT_hide() { + if (!DeveloperToolbar.visible) { + ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar"); + } + else { + DeveloperToolbar.display.inputter.setInput(""); + DeveloperToolbar.hide(); + } +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Test inputs + * typed: The text to type at the input + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * emptyParameters: Array of parameters still to type. e.g. [ "<message>" ] + * directTabText: Simple completion text + * arrowTabText: When the completion is not an extension (without arrow) + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +DeveloperToolbarTest.checkInputStatus = function DTT_checkInputStatus(checks) { + if (!checks.emptyParameters) { + checks.emptyParameters = []; + } + if (!checks.directTabText) { + checks.directTabText = ''; + } + if (!checks.arrowTabText) { + checks.arrowTabText = ''; + } + + var display = DeveloperToolbar.display; + + if (checks.typed) { + info('Starting tests for ' + checks.typed); + display.inputter.setInput(checks.typed); + } + else { + ok(false, "Missing typed for " + JSON.stringify(checks)); + return; + } + + if (checks.cursor) { + display.inputter.setCursor(checks.cursor) + } + + var cursor = checks.cursor ? checks.cursor.start : checks.typed.length; + + var requisition = display.requisition; + var completer = display.completer; + var actual = completer._getCompleterTemplateData(); + + /* + if (checks.input) { + is(display.inputter.element.value, + checks.input, + 'input'); + } + + if (checks.cursor) { + is(display.inputter.element.selectionStart, + checks.cursor, + 'cursor'); + } + */ + + if (checks.status) { + is(requisition.getStatus().toString(), + checks.status, + 'status'); + } + + if (checks.markup) { + var statusMarkup = requisition.getInputStatusMarkup(cursor); + var actualMarkup = statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + + is(checks.markup, + actualMarkup, + 'markup'); + } + + if (checks.emptyParameters) { + var actualParams = actual.emptyParameters; + is(actualParams.length, + checks.emptyParameters.length, + 'emptyParameters.length'); + + if (actualParams.length === checks.emptyParameters.length) { + for (var i = 0; i < actualParams.length; i++) { + is(actualParams[i].replace(/\u00a0/g, ' '), + checks.emptyParameters[i], + 'emptyParameters[' + i + ']'); + } + } + else { + info('Expected: [ \"' + actualParams.join('", "') + '" ]'); + } + } + + if (checks.directTabText) { + is(actual.directTabText, + checks.directTabText, + 'directTabText'); + } + + if (checks.arrowTabText) { + is(actual.arrowTabText, + ' \u00a0\u21E5 ' + checks.arrowTabText, + 'arrowTabText'); + } + + if (checks.args) { + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + ok(false, 'Unknown parameter: ' + paramName); + return; + } + + if (check.value) { + is(assignment.value, + check.value, + 'checkStatus value for ' + paramName); + } + + if (check.name) { + is(assignment.value.name, + check.name, + 'checkStatus name for ' + paramName); + } + + if (check.type) { + is(assignment.arg.type, + check.type, + 'checkStatus type for ' + paramName); + } + + if (check.arg) { + is(assignment.arg.toString(), + check.arg, + 'checkStatus arg for ' + paramName); + } + + if (check.status) { + is(assignment.getStatus().toString(), + check.status, + 'checkStatus status for ' + paramName); + } + + if (check.message) { + is(assignment.getMessage(), + check.message, + 'checkStatus message for ' + paramName); + } + }); + } +}; + +/** + * Execute a command: + * + * DeveloperToolbarTest.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // RegExp to test against textContent of output + * // (can also be array of RegExps) + * blankOutput: true, // Special checks when there is no output + * }); + */ +DeveloperToolbarTest.exec = function DTT_exec(tests) { + tests = tests || {}; + + if (tests.typed) { + DeveloperToolbar.display.inputter.setInput(tests.typed); + } + + let typed = DeveloperToolbar.display.inputter.getInputState().typed; + let output = DeveloperToolbar.display.requisition.exec(); + + is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + let expectedArg = tests.args[arg]; + let actualArg = output.args[arg]; + + if (typeof expectedArg === 'function') { + ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg); + } + else { + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (let i = 0; i < expectedArg.length; i++) { + is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + } + }); + } + + let displayed = DeveloperToolbar.outputPanel._div.textContent; + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + ok(false, "html output for " + typed + " against " + match.source + + " (textContent sent to info)"); + info("Actual textContent"); + info(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + ok(false, "html output for " + typed + " (textContent sent to info)"); + info("Actual textContent"); + info(displayed); + } + } +}; + +/** + * Quick wrapper around the things you need to do to run DeveloperToolbar + * command tests: + * - Set the pref 'devtools.toolbar.enabled' to true + * - Add a tab pointing at |uri| + * - Open the DeveloperToolbar + * - Register a cleanup function to undo the above + * - Run the tests + * + * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...' + * @param target Either a function or array of functions containing the tests + * to run. If an array of test function is passed then we will clear up after + * the tests have completed. If a single test function is passed then this + * function should arrange for 'finish()' to be called on completion. + */ +DeveloperToolbarTest.test = function DTT_test(uri, target) { + let menuItem = document.getElementById("menu_devToolbar"); + let command = document.getElementById("Tools:DevToolbar"); + let appMenuItem = document.getElementById("appmenu_devToolbar"); + + registerCleanupFunction(function() { + DeveloperToolbarTest.hide(); + + // a.k.a Services.prefs.clearUserPref("devtools.toolbar.enabled"); + if (menuItem) { + menuItem.hidden = true; + } + if (command) { + command.setAttribute("disabled", "true"); + } + if (appMenuItem) { + appMenuItem.hidden = true; + } + + // leakHunt({ DeveloperToolbar: DeveloperToolbar }); + }); + + // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true); + if (menuItem) { + menuItem.hidden = false; + } + if (command) { + command.removeAttribute("disabled"); + } + if (appMenuItem) { + appMenuItem.hidden = false; + } + + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + content.location = uri; + + let tab = gBrowser.selectedTab; + let browser = gBrowser.getBrowserForTab(tab); + + var onTabLoad = function() { + browser.removeEventListener("load", onTabLoad, true); + + DeveloperToolbarTest.show(function() { + if (helpers) { + helpers.setup({ display: DeveloperToolbar.display }); + } + + if (Array.isArray(target)) { + try { + target.forEach(function(func) { + func(browser, tab); + }) + } + finally { + DeveloperToolbarTest._checkFinish(); + } + } + else { + try { + target(browser, tab); + } + catch (ex) { + ok(false, "" + ex); + DeveloperToolbarTest._finish(); + throw ex; + } + } + }); + } + + browser.addEventListener("load", onTabLoad, true); +}; + +DeveloperToolbarTest._outstanding = []; + +DeveloperToolbarTest._checkFinish = function() { + info('_checkFinish. ' + DeveloperToolbarTest._outstanding.length + ' outstanding'); + if (DeveloperToolbarTest._outstanding.length == 0) { + DeveloperToolbarTest._finish(); + } +} + +DeveloperToolbarTest._finish = function() { + info('Finish'); + DeveloperToolbarTest.closeAllTabs(); + finish(); +} + +DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { + var todo = function() { + var reply = aFunc.apply(aScope, arguments); + DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { + return aJob != todo; + }); + DeveloperToolbarTest._checkFinish(); + return reply; + } + DeveloperToolbarTest._outstanding.push(todo); + return todo; +}; + +DeveloperToolbarTest.checkNotCalled = function(aMsg, aFunc, aScope) { + return function() { + ok(false, aMsg); + return aFunc.apply(aScope, arguments); + } +}; + +/** + * + */ +DeveloperToolbarTest.closeAllTabs = function() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}; + +/////////////////////////////////////////////////////////////////////////////// + +var helpers = {}; + +helpers._display = undefined; + +helpers.setup = function(options) { + helpers._display = options.display; + if (typeof ok !== 'undefined') { + test.ok = ok; + test.is = is; + test.log = info; + } +}; + +helpers.shutdown = function(options) { + helpers._display = undefined; +}; + +/** + * Various functions to return the actual state of the command line + */ +helpers._actual = { + input: function() { + return helpers._display.inputter.element.value; + }, + + hints: function() { + var templateData = helpers._display.completer._getCompleterTemplateData(); + var actualHints = templateData.directTabText + + templateData.emptyParameters.join('') + + templateData.arrowTabText; + return actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + }, + + markup: function() { + var cursor = helpers._display.inputter.element.selectionStart; + var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor); + return statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + }, + + cursor: function() { + return helpers._display.inputter.element.selectionStart; + }, + + current: function() { + return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name; + }, + + status: function() { + return helpers._display.requisition.getStatus().toString(); + }, + + outputState: function() { + var outputData = helpers._display.focusManager._shouldShowOutput(); + return outputData.visible + ':' + outputData.reason; + }, + + tooltipState: function() { + var tooltipData = helpers._display.focusManager._shouldShowTooltip(); + return tooltipData.visible + ':' + tooltipData.reason; + } +}; + +helpers._directToString = [ 'boolean', 'undefined', 'number' ]; + +helpers._createDebugCheck = function() { + var requisition = helpers._display.requisition; + var command = requisition.commandAssignment.value; + var input = helpers._actual.input(); + var padding = Array(input.length + 1).join(' '); + + var output = ''; + output += 'helpers.setInput(\'' + input + '\');\n'; + output += 'helpers.check({\n'; + output += ' input: \'' + input + '\',\n'; + output += ' hints: ' + padding + '\'' + helpers._actual.hints() + '\',\n'; + output += ' markup: \'' + helpers._actual.markup() + '\',\n'; + output += ' cursor: ' + helpers._actual.cursor() + ',\n'; + output += ' current: \'' + helpers._actual.current() + '\',\n'; + output += ' status: \'' + helpers._actual.status() + '\',\n'; + output += ' outputState: \'' + helpers._actual.outputState() + '\',\n'; + + if (command) { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\',\n'; + output += ' args: {\n'; + output += ' command: { name: \'' + command.name + '\' },\n'; + + requisition.getAssignments().forEach(function(assignment) { + output += ' ' + assignment.param.name + ': { '; + + if (typeof assignment.value === 'string') { + output += 'value: \'' + assignment.value + '\', '; + } + else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) { + output += 'value: ' + assignment.value + ', '; + } + else if (assignment.value === null) { + output += 'value: ' + assignment.value + ', '; + } + else { + output += '/*value:' + assignment.value + ',*/ '; + } + + output += 'arg: \'' + assignment.arg + '\', '; + output += 'status: \'' + assignment.getStatus().toString() + '\', '; + output += 'message: \'' + assignment.getMessage() + '\''; + output += ' },\n'; + }); + + output += ' }\n'; + } + else { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\'\n'; + } + output += '});'; + + return output; +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +helpers.setInput = function(typed, cursor) { + helpers._display.inputter.setInput(typed); + + if (cursor) { + helpers._display.inputter.setCursor({ start: cursor, end: cursor }); + } + + helpers._display.focusManager.onInputChange(); + + test.log('setInput("' + typed + '"' + (cursor == null ? '' : ', ' + cursor) + ')'); +}; + +/** + * Simulate focusing the input field + */ +helpers.focusInput = function() { + helpers._display.inputter.focus(); +}; + +/** + * Simulate pressing TAB in the input field + */ +helpers.pressTab = function() { + helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/); +}; + +/** + * Simulate pressing RETURN in the input field + */ +helpers.pressReturn = function() { + helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/); +}; + +/** + * Simulate pressing a key by keyCode in the input field + */ +helpers.pressKey = function(keyCode) { + var fakeEvent = { + keyCode: keyCode, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + helpers._display.inputter.onKeyDown(fakeEvent); + helpers._display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +helpers.check = function(checks) { + if ('input' in checks) { + test.is(helpers._actual.input(), checks.input, 'input'); + } + + if ('cursor' in checks) { + test.is(helpers._actual.cursor(), checks.cursor, 'cursor'); + } + + if ('current' in checks) { + test.is(helpers._actual.current(), checks.current, 'current'); + } + + if ('status' in checks) { + test.is(helpers._actual.status(), checks.status, 'status'); + } + + if ('markup' in checks) { + test.is(helpers._actual.markup(), checks.markup, 'markup'); + } + + if ('hints' in checks) { + test.is(helpers._actual.hints(), checks.hints, 'hints'); + } + + if ('tooltipState' in checks) { + test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState'); + } + + if ('outputState' in checks) { + test.is(helpers._actual.outputState(), checks.outputState, 'outputState'); + } + + if (checks.args != null) { + var requisition = helpers._display.requisition; + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown arg: ' + paramName); + return; + } + + if ('value' in check) { + test.is(assignment.value, + check.value, + 'arg.' + paramName + '.value'); + } + + if ('name' in check) { + test.is(assignment.value.name, + check.name, + 'arg.' + paramName + '.name'); + } + + if ('type' in check) { + test.is(assignment.arg.type, + check.type, + 'arg.' + paramName + '.type'); + } + + if ('arg' in check) { + test.is(assignment.arg.toString(), + check.arg, + 'arg.' + paramName + '.arg'); + } + + if ('status' in check) { + test.is(assignment.getStatus().toString(), + check.status, + 'arg.' + paramName + '.status'); + } + + if ('message' in check) { + test.is(assignment.getMessage(), + check.message, + 'arg.' + paramName + '.message'); + } + }); + } +}; + +/** + * Execute a command: + * + * helpers.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // Regex to test against textContent of output + * blankOutput: true, // Special checks when there is no output + * }); + */ +helpers.exec = function(tests) { + var requisition = helpers._display.requisition; + var inputter = helpers._display.inputter; + + tests = tests || {}; + + if (tests.typed) { + inputter.setInput(tests.typed); + } + + var typed = inputter.getInputState().typed; + var output = requisition.exec({ hidden: true }); + + test.is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + test.ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // test.ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + test.is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + var expectedArg = tests.args[arg]; + var actualArg = output.args[arg]; + + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + test.ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + test.is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (var i = 0; i < expectedArg.length; i++) { + test.is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + test.is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + }); + } + + if (!options.window.document.createElement) { + test.log('skipping output tests (missing doc.createElement) for ' + typed); + return; + } + + var div = options.window.document.createElement('div'); + output.toDom(div); + var displayed = div.textContent.trim(); + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + test.ok(false, "html output for " + typed + " against " + match.source); + console.log("Actual textContent"); + console.log(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + test.ok(false, "html for " + typed + " (textContent sent to info)"); + console.log("Actual textContent"); + console.log(displayed); + } + } +};
--- a/browser/devtools/highlighter/test/Makefile.in +++ b/browser/devtools/highlighter/test/Makefile.in @@ -35,13 +35,13 @@ include $(topsrcdir)/config/rules.mk browser_inspector_menu.js \ browser_inspector_pseudoclass_lock.js \ browser_inspector_pseudoClass_menu.js \ browser_inspector_destroyselection.html \ browser_inspector_destroyselection.js \ browser_inspector_cmd_inspect.js \ browser_inspector_cmd_inspect.html \ head.js \ - helper.js \ + helpers.js \ $(NULL) libs:: $(_BROWSER_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/highlighter/test/browser_inspector_cmd_inspect.js +++ b/browser/devtools/highlighter/test/browser_inspector_cmd_inspect.js @@ -6,60 +6,115 @@ const TEST_URI = "http://example.com/browser/browser/devtools/highlighter/" + "test/browser_inspector_cmd_inspect.html"; function test() { DeveloperToolbarTest.test(TEST_URI, [ testInspect ]); } function testInspect() { - DeveloperToolbarTest.checkInputStatus({ - typed: "inspec", - directTabText: "t", - status: "ERROR" + helpers.setInput('inspect'); + helpers.check({ + input: 'inspect', + hints: ' <node>', + markup: 'VVVVVVV', + status: 'ERROR', + args: { + node: { message: '' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect", - emptyParameters: [ " <node>" ], - status: "ERROR" + helpers.setInput('inspect h1'); + helpers.check({ + input: 'inspect h1', + hints: '', + markup: 'VVVVVVVVII', + status: 'ERROR', + args: { + node: { message: 'No matches' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect h1", - status: "ERROR" + helpers.setInput('inspect span'); + helpers.check({ + input: 'inspect span', + hints: '', + markup: 'VVVVVVVVEEEE', + status: 'ERROR', + args: { + node: { message: 'Too many matches (2)' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect span", - status: "ERROR" + helpers.setInput('inspect div'); + helpers.check({ + input: 'inspect div', + hints: '', + markup: 'VVVVVVVVVVV', + status: 'VALID', + args: { + node: { message: '' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect div", - status: "VALID" + helpers.setInput('inspect .someclas'); + helpers.check({ + input: 'inspect .someclas', + hints: '', + markup: 'VVVVVVVVIIIIIIIII', + status: 'ERROR', + args: { + node: { message: 'No matches' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect .someclass", - status: "VALID" - }); - - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect #someid", - status: "VALID" + helpers.setInput('inspect .someclass'); + helpers.check({ + input: 'inspect .someclass', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + node: { message: '' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect button[disabled]", - status: "VALID" + helpers.setInput('inspect #someid'); + helpers.check({ + input: 'inspect #someid', + hints: '', + markup: 'VVVVVVVVVVVVVVV', + status: 'VALID', + args: { + node: { message: '' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect p>strong", - status: "VALID" + helpers.setInput('inspect button[disabled]'); + helpers.check({ + input: 'inspect button[disabled]', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + node: { message: '' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "inspect :root", - status: "VALID" + helpers.setInput('inspect p>strong'); + helpers.check({ + input: 'inspect p>strong', + hints: '', + markup: 'VVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + node: { message: '' }, + } + }); + + helpers.setInput('inspect :root'); + helpers.check({ + input: 'inspect :root', + hints: '', + markup: 'VVVVVVVVVVVVV', + status: 'VALID' }); }
--- a/browser/devtools/highlighter/test/head.js +++ b/browser/devtools/highlighter/test/head.js @@ -4,17 +4,17 @@ const Cu = Components.utils; let tempScope = {}; Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope); let LayoutHelpers = tempScope.LayoutHelpers; // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); -Services.scriptloader.loadSubScript(testDir + "/helper.js", this); +Services.scriptloader.loadSubScript(testDir + "/helpers.js", this); // Clear preferences that may be set during the course of tests. function clearUserPrefs() { Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen"); Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); }
rename from browser/devtools/highlighter/test/helper.js rename to browser/devtools/highlighter/test/helpers.js --- a/browser/devtools/highlighter/test/helper.js +++ b/browser/devtools/highlighter/test/helpers.js @@ -26,16 +26,43 @@ * * * * FOR A LONG TIME. * */ +/* + * Use as a JSM + * ------------ + * helpers._createDebugCheck() and maybe other functions in this file can be + * useful at runtime, so it is possible to use helpers.js as a JSM. + * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to + * DeveloperToolbar.jsm the following: + * + * XPCOMUtils.defineLazyModuleGetter(this, "helpers", + * "resource:///modules/devtools/helpers.jsm"); + * + * At the bottom of DeveloperToolbar.prototype._onload add this: + * + * var options = { display: this.display }; + * this._input.onkeypress = function(ev) { + * helpers.setup(options); + * dump(helpers._createDebugCheck() + '\n\n'); + * }; + * + * Now GCLI will emit output on every keypress that both explains the state + * of GCLI and can be run as a test case. + */ + +var EXPORTED_SYMBOLS = [ 'helpers' ]; + +var test = { }; + /** * Various functions for testing DeveloperToolbar. * Parts of this code exist in: * - browser/devtools/commandline/test/head.js * - browser/devtools/shared/test/head.js */ let DeveloperToolbarTest = { }; @@ -158,16 +185,19 @@ DeveloperToolbarTest.checkInputStatus = if (actualParams.length === checks.emptyParameters.length) { for (var i = 0; i < actualParams.length; i++) { is(actualParams[i].replace(/\u00a0/g, ' '), checks.emptyParameters[i], 'emptyParameters[' + i + ']'); } } + else { + info('Expected: [ \"' + actualParams.join('", "') + '" ]'); + } } if (checks.directTabText) { is(actual.directTabText, checks.directTabText, 'directTabText'); } @@ -385,16 +415,20 @@ DeveloperToolbarTest.test = function DTT let tab = gBrowser.selectedTab; let browser = gBrowser.getBrowserForTab(tab); var onTabLoad = function() { browser.removeEventListener("load", onTabLoad, true); DeveloperToolbarTest.show(function() { + if (helpers) { + helpers.setup({ display: DeveloperToolbar.display }); + } + if (Array.isArray(target)) { try { target.forEach(function(func) { func(browser, tab); }) } finally { DeveloperToolbarTest._checkFinish(); @@ -414,22 +448,24 @@ DeveloperToolbarTest.test = function DTT } browser.addEventListener("load", onTabLoad, true); }; DeveloperToolbarTest._outstanding = []; DeveloperToolbarTest._checkFinish = function() { + info('_checkFinish. ' + DeveloperToolbarTest._outstanding.length + ' outstanding'); if (DeveloperToolbarTest._outstanding.length == 0) { DeveloperToolbarTest._finish(); } } DeveloperToolbarTest._finish = function() { + info('Finish'); DeveloperToolbarTest.closeAllTabs(); finish(); } DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { var todo = function() { var reply = aFunc.apply(aScope, arguments); DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { @@ -452,8 +488,394 @@ DeveloperToolbarTest.checkNotCalled = fu /** * */ DeveloperToolbarTest.closeAllTabs = function() { while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } }; + +/////////////////////////////////////////////////////////////////////////////// + +var helpers = {}; + +helpers._display = undefined; + +helpers.setup = function(options) { + helpers._display = options.display; + if (typeof ok !== 'undefined') { + test.ok = ok; + test.is = is; + test.log = info; + } +}; + +helpers.shutdown = function(options) { + helpers._display = undefined; +}; + +/** + * Various functions to return the actual state of the command line + */ +helpers._actual = { + input: function() { + return helpers._display.inputter.element.value; + }, + + hints: function() { + var templateData = helpers._display.completer._getCompleterTemplateData(); + var actualHints = templateData.directTabText + + templateData.emptyParameters.join('') + + templateData.arrowTabText; + return actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + }, + + markup: function() { + var cursor = helpers._display.inputter.element.selectionStart; + var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor); + return statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + }, + + cursor: function() { + return helpers._display.inputter.element.selectionStart; + }, + + current: function() { + return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name; + }, + + status: function() { + return helpers._display.requisition.getStatus().toString(); + }, + + outputState: function() { + var outputData = helpers._display.focusManager._shouldShowOutput(); + return outputData.visible + ':' + outputData.reason; + }, + + tooltipState: function() { + var tooltipData = helpers._display.focusManager._shouldShowTooltip(); + return tooltipData.visible + ':' + tooltipData.reason; + } +}; + +helpers._directToString = [ 'boolean', 'undefined', 'number' ]; + +helpers._createDebugCheck = function() { + var requisition = helpers._display.requisition; + var command = requisition.commandAssignment.value; + var input = helpers._actual.input(); + var padding = Array(input.length + 1).join(' '); + + var output = ''; + output += 'helpers.setInput(\'' + input + '\');\n'; + output += 'helpers.check({\n'; + output += ' input: \'' + input + '\',\n'; + output += ' hints: ' + padding + '\'' + helpers._actual.hints() + '\',\n'; + output += ' markup: \'' + helpers._actual.markup() + '\',\n'; + output += ' cursor: ' + helpers._actual.cursor() + ',\n'; + output += ' current: \'' + helpers._actual.current() + '\',\n'; + output += ' status: \'' + helpers._actual.status() + '\',\n'; + output += ' outputState: \'' + helpers._actual.outputState() + '\',\n'; + + if (command) { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\',\n'; + output += ' args: {\n'; + output += ' command: { name: \'' + command.name + '\' },\n'; + + requisition.getAssignments().forEach(function(assignment) { + output += ' ' + assignment.param.name + ': { '; + + if (typeof assignment.value === 'string') { + output += 'value: \'' + assignment.value + '\', '; + } + else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) { + output += 'value: ' + assignment.value + ', '; + } + else if (assignment.value === null) { + output += 'value: ' + assignment.value + ', '; + } + else { + output += '/*value:' + assignment.value + ',*/ '; + } + + output += 'arg: \'' + assignment.arg + '\', '; + output += 'status: \'' + assignment.getStatus().toString() + '\', '; + output += 'message: \'' + assignment.getMessage() + '\''; + output += ' },\n'; + }); + + output += ' }\n'; + } + else { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\'\n'; + } + output += '});'; + + return output; +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +helpers.setInput = function(typed, cursor) { + helpers._display.inputter.setInput(typed); + + if (cursor) { + helpers._display.inputter.setCursor({ start: cursor, end: cursor }); + } + + helpers._display.focusManager.onInputChange(); + + test.log('setInput("' + typed + '"' + (cursor == null ? '' : ', ' + cursor) + ')'); +}; + +/** + * Simulate focusing the input field + */ +helpers.focusInput = function() { + helpers._display.inputter.focus(); +}; + +/** + * Simulate pressing TAB in the input field + */ +helpers.pressTab = function() { + helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/); +}; + +/** + * Simulate pressing RETURN in the input field + */ +helpers.pressReturn = function() { + helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/); +}; + +/** + * Simulate pressing a key by keyCode in the input field + */ +helpers.pressKey = function(keyCode) { + var fakeEvent = { + keyCode: keyCode, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + helpers._display.inputter.onKeyDown(fakeEvent); + helpers._display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +helpers.check = function(checks) { + if ('input' in checks) { + test.is(helpers._actual.input(), checks.input, 'input'); + } + + if ('cursor' in checks) { + test.is(helpers._actual.cursor(), checks.cursor, 'cursor'); + } + + if ('current' in checks) { + test.is(helpers._actual.current(), checks.current, 'current'); + } + + if ('status' in checks) { + test.is(helpers._actual.status(), checks.status, 'status'); + } + + if ('markup' in checks) { + test.is(helpers._actual.markup(), checks.markup, 'markup'); + } + + if ('hints' in checks) { + test.is(helpers._actual.hints(), checks.hints, 'hints'); + } + + if ('tooltipState' in checks) { + test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState'); + } + + if ('outputState' in checks) { + test.is(helpers._actual.outputState(), checks.outputState, 'outputState'); + } + + if (checks.args != null) { + var requisition = helpers._display.requisition; + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown arg: ' + paramName); + return; + } + + if ('value' in check) { + test.is(assignment.value, + check.value, + 'arg.' + paramName + '.value'); + } + + if ('name' in check) { + test.is(assignment.value.name, + check.name, + 'arg.' + paramName + '.name'); + } + + if ('type' in check) { + test.is(assignment.arg.type, + check.type, + 'arg.' + paramName + '.type'); + } + + if ('arg' in check) { + test.is(assignment.arg.toString(), + check.arg, + 'arg.' + paramName + '.arg'); + } + + if ('status' in check) { + test.is(assignment.getStatus().toString(), + check.status, + 'arg.' + paramName + '.status'); + } + + if ('message' in check) { + test.is(assignment.getMessage(), + check.message, + 'arg.' + paramName + '.message'); + } + }); + } +}; + +/** + * Execute a command: + * + * helpers.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // Regex to test against textContent of output + * blankOutput: true, // Special checks when there is no output + * }); + */ +helpers.exec = function(tests) { + var requisition = helpers._display.requisition; + var inputter = helpers._display.inputter; + + tests = tests || {}; + + if (tests.typed) { + inputter.setInput(tests.typed); + } + + var typed = inputter.getInputState().typed; + var output = requisition.exec({ hidden: true }); + + test.is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + test.ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // test.ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + test.is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + var expectedArg = tests.args[arg]; + var actualArg = output.args[arg]; + + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + test.ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + test.is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (var i = 0; i < expectedArg.length; i++) { + test.is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + test.is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + }); + } + + if (!options.window.document.createElement) { + test.log('skipping output tests (missing doc.createElement) for ' + typed); + return; + } + + var div = options.window.document.createElement('div'); + output.toDom(div); + var displayed = div.textContent.trim(); + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + test.ok(false, "html output for " + typed + " against " + match.source); + console.log("Actual textContent"); + console.log(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + test.ok(false, "html for " + typed + " (textContent sent to info)"); + console.log("Actual textContent"); + console.log(displayed); + } + } +};
--- a/browser/devtools/responsivedesign/test/Makefile.in +++ b/browser/devtools/responsivedesign/test/Makefile.in @@ -45,14 +45,14 @@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk _BROWSER_FILES = \ browser_responsiveui.js \ browser_responsiveruleview.js \ browser_responsive_cmd.js \ browser_responsivecomputedview.js \ head.js \ - helper.js \ + helpers.js \ $(NULL) libs:: $(_BROWSER_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/responsivedesign/test/browser_responsive_cmd.js +++ b/browser/devtools/responsivedesign/test/browser_responsive_cmd.js @@ -9,52 +9,78 @@ function isOpen() { return !!gBrowser.selectedTab.__responsiveUI; } function isClosed() { return !isOpen(); } function GAT_test() { - DeveloperToolbarTest.checkInputStatus({ - typed: "resize toggle", - status: "VALID" + helpers.setInput('resize toggle'); + helpers.check({ + input: 'resize toggle', + hints: '', + markup: 'VVVVVVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); ok(isOpen(), "responsive mode is open"); - DeveloperToolbarTest.checkInputStatus({ - typed: "resize toggle", - status: "VALID" + helpers.setInput('resize toggle'); + helpers.check({ + input: 'resize toggle', + hints: '', + markup: 'VVVVVVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); ok(isClosed(), "responsive mode is closed"); - DeveloperToolbarTest.checkInputStatus({ - typed: "resize on", - status: "VALID" + helpers.setInput('resize on'); + helpers.check({ + input: 'resize on', + hints: '', + markup: 'VVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); ok(isOpen(), "responsive mode is open"); - DeveloperToolbarTest.checkInputStatus({ - typed: "resize off", - status: "VALID" + helpers.setInput('resize off'); + helpers.check({ + input: 'resize off', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); ok(isClosed(), "responsive mode is closed"); - DeveloperToolbarTest.checkInputStatus({ - typed: "resize to 400 400", - status: "VALID" + helpers.setInput('resize to 400 400'); + helpers.check({ + input: 'resize to 400 400', + hints: '', + markup: 'VVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + width: { value: 400 }, + height: { value: 400 }, + } }); + DeveloperToolbarTest.exec(); ok(isOpen(), "responsive mode is open"); - DeveloperToolbarTest.checkInputStatus({ - typed: "resize off", - status: "VALID" + helpers.setInput('resize off'); + helpers.check({ + input: 'resize off', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' }); + DeveloperToolbarTest.exec(); ok(isClosed(), "responsive mode is closed"); - - // executeSoon(finish); }
--- a/browser/devtools/responsivedesign/test/head.js +++ b/browser/devtools/responsivedesign/test/head.js @@ -1,8 +1,8 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); -Services.scriptloader.loadSubScript(testDir + "/helper.js", this); +Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
rename from browser/devtools/responsivedesign/test/helper.js rename to browser/devtools/responsivedesign/test/helpers.js --- a/browser/devtools/responsivedesign/test/helper.js +++ b/browser/devtools/responsivedesign/test/helpers.js @@ -26,16 +26,43 @@ * * * * FOR A LONG TIME. * */ +/* + * Use as a JSM + * ------------ + * helpers._createDebugCheck() and maybe other functions in this file can be + * useful at runtime, so it is possible to use helpers.js as a JSM. + * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to + * DeveloperToolbar.jsm the following: + * + * XPCOMUtils.defineLazyModuleGetter(this, "helpers", + * "resource:///modules/devtools/helpers.jsm"); + * + * At the bottom of DeveloperToolbar.prototype._onload add this: + * + * var options = { display: this.display }; + * this._input.onkeypress = function(ev) { + * helpers.setup(options); + * dump(helpers._createDebugCheck() + '\n\n'); + * }; + * + * Now GCLI will emit output on every keypress that both explains the state + * of GCLI and can be run as a test case. + */ + +var EXPORTED_SYMBOLS = [ 'helpers' ]; + +var test = { }; + /** * Various functions for testing DeveloperToolbar. * Parts of this code exist in: * - browser/devtools/commandline/test/head.js * - browser/devtools/shared/test/head.js */ let DeveloperToolbarTest = { }; @@ -158,16 +185,19 @@ DeveloperToolbarTest.checkInputStatus = if (actualParams.length === checks.emptyParameters.length) { for (var i = 0; i < actualParams.length; i++) { is(actualParams[i].replace(/\u00a0/g, ' '), checks.emptyParameters[i], 'emptyParameters[' + i + ']'); } } + else { + info('Expected: [ \"' + actualParams.join('", "') + '" ]'); + } } if (checks.directTabText) { is(actual.directTabText, checks.directTabText, 'directTabText'); } @@ -385,16 +415,20 @@ DeveloperToolbarTest.test = function DTT let tab = gBrowser.selectedTab; let browser = gBrowser.getBrowserForTab(tab); var onTabLoad = function() { browser.removeEventListener("load", onTabLoad, true); DeveloperToolbarTest.show(function() { + if (helpers) { + helpers.setup({ display: DeveloperToolbar.display }); + } + if (Array.isArray(target)) { try { target.forEach(function(func) { func(browser, tab); }) } finally { DeveloperToolbarTest._checkFinish(); @@ -414,22 +448,24 @@ DeveloperToolbarTest.test = function DTT } browser.addEventListener("load", onTabLoad, true); }; DeveloperToolbarTest._outstanding = []; DeveloperToolbarTest._checkFinish = function() { + info('_checkFinish. ' + DeveloperToolbarTest._outstanding.length + ' outstanding'); if (DeveloperToolbarTest._outstanding.length == 0) { DeveloperToolbarTest._finish(); } } DeveloperToolbarTest._finish = function() { + info('Finish'); DeveloperToolbarTest.closeAllTabs(); finish(); } DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { var todo = function() { var reply = aFunc.apply(aScope, arguments); DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { @@ -452,8 +488,394 @@ DeveloperToolbarTest.checkNotCalled = fu /** * */ DeveloperToolbarTest.closeAllTabs = function() { while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } }; + +/////////////////////////////////////////////////////////////////////////////// + +var helpers = {}; + +helpers._display = undefined; + +helpers.setup = function(options) { + helpers._display = options.display; + if (typeof ok !== 'undefined') { + test.ok = ok; + test.is = is; + test.log = info; + } +}; + +helpers.shutdown = function(options) { + helpers._display = undefined; +}; + +/** + * Various functions to return the actual state of the command line + */ +helpers._actual = { + input: function() { + return helpers._display.inputter.element.value; + }, + + hints: function() { + var templateData = helpers._display.completer._getCompleterTemplateData(); + var actualHints = templateData.directTabText + + templateData.emptyParameters.join('') + + templateData.arrowTabText; + return actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + }, + + markup: function() { + var cursor = helpers._display.inputter.element.selectionStart; + var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor); + return statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + }, + + cursor: function() { + return helpers._display.inputter.element.selectionStart; + }, + + current: function() { + return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name; + }, + + status: function() { + return helpers._display.requisition.getStatus().toString(); + }, + + outputState: function() { + var outputData = helpers._display.focusManager._shouldShowOutput(); + return outputData.visible + ':' + outputData.reason; + }, + + tooltipState: function() { + var tooltipData = helpers._display.focusManager._shouldShowTooltip(); + return tooltipData.visible + ':' + tooltipData.reason; + } +}; + +helpers._directToString = [ 'boolean', 'undefined', 'number' ]; + +helpers._createDebugCheck = function() { + var requisition = helpers._display.requisition; + var command = requisition.commandAssignment.value; + var input = helpers._actual.input(); + var padding = Array(input.length + 1).join(' '); + + var output = ''; + output += 'helpers.setInput(\'' + input + '\');\n'; + output += 'helpers.check({\n'; + output += ' input: \'' + input + '\',\n'; + output += ' hints: ' + padding + '\'' + helpers._actual.hints() + '\',\n'; + output += ' markup: \'' + helpers._actual.markup() + '\',\n'; + output += ' cursor: ' + helpers._actual.cursor() + ',\n'; + output += ' current: \'' + helpers._actual.current() + '\',\n'; + output += ' status: \'' + helpers._actual.status() + '\',\n'; + output += ' outputState: \'' + helpers._actual.outputState() + '\',\n'; + + if (command) { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\',\n'; + output += ' args: {\n'; + output += ' command: { name: \'' + command.name + '\' },\n'; + + requisition.getAssignments().forEach(function(assignment) { + output += ' ' + assignment.param.name + ': { '; + + if (typeof assignment.value === 'string') { + output += 'value: \'' + assignment.value + '\', '; + } + else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) { + output += 'value: ' + assignment.value + ', '; + } + else if (assignment.value === null) { + output += 'value: ' + assignment.value + ', '; + } + else { + output += '/*value:' + assignment.value + ',*/ '; + } + + output += 'arg: \'' + assignment.arg + '\', '; + output += 'status: \'' + assignment.getStatus().toString() + '\', '; + output += 'message: \'' + assignment.getMessage() + '\''; + output += ' },\n'; + }); + + output += ' }\n'; + } + else { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\'\n'; + } + output += '});'; + + return output; +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +helpers.setInput = function(typed, cursor) { + helpers._display.inputter.setInput(typed); + + if (cursor) { + helpers._display.inputter.setCursor({ start: cursor, end: cursor }); + } + + helpers._display.focusManager.onInputChange(); + + test.log('setInput("' + typed + '"' + (cursor == null ? '' : ', ' + cursor) + ')'); +}; + +/** + * Simulate focusing the input field + */ +helpers.focusInput = function() { + helpers._display.inputter.focus(); +}; + +/** + * Simulate pressing TAB in the input field + */ +helpers.pressTab = function() { + helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/); +}; + +/** + * Simulate pressing RETURN in the input field + */ +helpers.pressReturn = function() { + helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/); +}; + +/** + * Simulate pressing a key by keyCode in the input field + */ +helpers.pressKey = function(keyCode) { + var fakeEvent = { + keyCode: keyCode, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + helpers._display.inputter.onKeyDown(fakeEvent); + helpers._display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +helpers.check = function(checks) { + if ('input' in checks) { + test.is(helpers._actual.input(), checks.input, 'input'); + } + + if ('cursor' in checks) { + test.is(helpers._actual.cursor(), checks.cursor, 'cursor'); + } + + if ('current' in checks) { + test.is(helpers._actual.current(), checks.current, 'current'); + } + + if ('status' in checks) { + test.is(helpers._actual.status(), checks.status, 'status'); + } + + if ('markup' in checks) { + test.is(helpers._actual.markup(), checks.markup, 'markup'); + } + + if ('hints' in checks) { + test.is(helpers._actual.hints(), checks.hints, 'hints'); + } + + if ('tooltipState' in checks) { + test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState'); + } + + if ('outputState' in checks) { + test.is(helpers._actual.outputState(), checks.outputState, 'outputState'); + } + + if (checks.args != null) { + var requisition = helpers._display.requisition; + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown arg: ' + paramName); + return; + } + + if ('value' in check) { + test.is(assignment.value, + check.value, + 'arg.' + paramName + '.value'); + } + + if ('name' in check) { + test.is(assignment.value.name, + check.name, + 'arg.' + paramName + '.name'); + } + + if ('type' in check) { + test.is(assignment.arg.type, + check.type, + 'arg.' + paramName + '.type'); + } + + if ('arg' in check) { + test.is(assignment.arg.toString(), + check.arg, + 'arg.' + paramName + '.arg'); + } + + if ('status' in check) { + test.is(assignment.getStatus().toString(), + check.status, + 'arg.' + paramName + '.status'); + } + + if ('message' in check) { + test.is(assignment.getMessage(), + check.message, + 'arg.' + paramName + '.message'); + } + }); + } +}; + +/** + * Execute a command: + * + * helpers.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // Regex to test against textContent of output + * blankOutput: true, // Special checks when there is no output + * }); + */ +helpers.exec = function(tests) { + var requisition = helpers._display.requisition; + var inputter = helpers._display.inputter; + + tests = tests || {}; + + if (tests.typed) { + inputter.setInput(tests.typed); + } + + var typed = inputter.getInputState().typed; + var output = requisition.exec({ hidden: true }); + + test.is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + test.ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // test.ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + test.is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + var expectedArg = tests.args[arg]; + var actualArg = output.args[arg]; + + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + test.ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + test.is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (var i = 0; i < expectedArg.length; i++) { + test.is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + test.is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + }); + } + + if (!options.window.document.createElement) { + test.log('skipping output tests (missing doc.createElement) for ' + typed); + return; + } + + var div = options.window.document.createElement('div'); + output.toDom(div); + var displayed = div.textContent.trim(); + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + test.ok(false, "html output for " + typed + " against " + match.source); + console.log("Actual textContent"); + console.log(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + test.ok(false, "html for " + typed + " (textContent sent to info)"); + console.log("Actual textContent"); + console.log(displayed); + } + } +};
--- a/browser/devtools/shared/test/Makefile.in +++ b/browser/devtools/shared/test/Makefile.in @@ -16,17 +16,17 @@ MOCHITEST_BROWSER_FILES = \ browser_promise_basic.js \ browser_require_basic.js \ browser_templater_basic.js \ browser_toolbar_basic.js \ browser_toolbar_tooltip.js \ browser_toolbar_webconsole_errors_count.js \ browser_layoutHelpers.js \ head.js \ - helper.js \ + helpers.js \ leakhunt.js \ $(NULL) MOCHITEST_BROWSER_FILES += \ browser_templater_basic.html \ browser_toolbar_basic.html \ browser_toolbar_webconsole_errors_count.html \ browser_layoutHelpers.html \
--- a/browser/devtools/shared/test/head.js +++ b/browser/devtools/shared/test/head.js @@ -5,17 +5,17 @@ let console = (function() { let tempScope = {}; Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); return tempScope.console; })(); // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); -Services.scriptloader.loadSubScript(testDir + "/helper.js", this); +Services.scriptloader.loadSubScript(testDir + "/helpers.js", this); /** * Open a new tab at a URL and call a callback on load */ function addTab(aURL, aCallback) { waitForExplicitFinish();
rename from browser/devtools/shared/test/helper.js rename to browser/devtools/shared/test/helpers.js --- a/browser/devtools/shared/test/helper.js +++ b/browser/devtools/shared/test/helpers.js @@ -26,16 +26,43 @@ * * * * FOR A LONG TIME. * */ +/* + * Use as a JSM + * ------------ + * helpers._createDebugCheck() and maybe other functions in this file can be + * useful at runtime, so it is possible to use helpers.js as a JSM. + * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to + * DeveloperToolbar.jsm the following: + * + * XPCOMUtils.defineLazyModuleGetter(this, "helpers", + * "resource:///modules/devtools/helpers.jsm"); + * + * At the bottom of DeveloperToolbar.prototype._onload add this: + * + * var options = { display: this.display }; + * this._input.onkeypress = function(ev) { + * helpers.setup(options); + * dump(helpers._createDebugCheck() + '\n\n'); + * }; + * + * Now GCLI will emit output on every keypress that both explains the state + * of GCLI and can be run as a test case. + */ + +var EXPORTED_SYMBOLS = [ 'helpers' ]; + +var test = { }; + /** * Various functions for testing DeveloperToolbar. * Parts of this code exist in: * - browser/devtools/commandline/test/head.js * - browser/devtools/shared/test/head.js */ let DeveloperToolbarTest = { }; @@ -158,16 +185,19 @@ DeveloperToolbarTest.checkInputStatus = if (actualParams.length === checks.emptyParameters.length) { for (var i = 0; i < actualParams.length; i++) { is(actualParams[i].replace(/\u00a0/g, ' '), checks.emptyParameters[i], 'emptyParameters[' + i + ']'); } } + else { + info('Expected: [ \"' + actualParams.join('", "') + '" ]'); + } } if (checks.directTabText) { is(actual.directTabText, checks.directTabText, 'directTabText'); } @@ -385,16 +415,20 @@ DeveloperToolbarTest.test = function DTT let tab = gBrowser.selectedTab; let browser = gBrowser.getBrowserForTab(tab); var onTabLoad = function() { browser.removeEventListener("load", onTabLoad, true); DeveloperToolbarTest.show(function() { + if (helpers) { + helpers.setup({ display: DeveloperToolbar.display }); + } + if (Array.isArray(target)) { try { target.forEach(function(func) { func(browser, tab); }) } finally { DeveloperToolbarTest._checkFinish(); @@ -414,22 +448,24 @@ DeveloperToolbarTest.test = function DTT } browser.addEventListener("load", onTabLoad, true); }; DeveloperToolbarTest._outstanding = []; DeveloperToolbarTest._checkFinish = function() { + info('_checkFinish. ' + DeveloperToolbarTest._outstanding.length + ' outstanding'); if (DeveloperToolbarTest._outstanding.length == 0) { DeveloperToolbarTest._finish(); } } DeveloperToolbarTest._finish = function() { + info('Finish'); DeveloperToolbarTest.closeAllTabs(); finish(); } DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { var todo = function() { var reply = aFunc.apply(aScope, arguments); DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { @@ -452,8 +488,394 @@ DeveloperToolbarTest.checkNotCalled = fu /** * */ DeveloperToolbarTest.closeAllTabs = function() { while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } }; + +/////////////////////////////////////////////////////////////////////////////// + +var helpers = {}; + +helpers._display = undefined; + +helpers.setup = function(options) { + helpers._display = options.display; + if (typeof ok !== 'undefined') { + test.ok = ok; + test.is = is; + test.log = info; + } +}; + +helpers.shutdown = function(options) { + helpers._display = undefined; +}; + +/** + * Various functions to return the actual state of the command line + */ +helpers._actual = { + input: function() { + return helpers._display.inputter.element.value; + }, + + hints: function() { + var templateData = helpers._display.completer._getCompleterTemplateData(); + var actualHints = templateData.directTabText + + templateData.emptyParameters.join('') + + templateData.arrowTabText; + return actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + }, + + markup: function() { + var cursor = helpers._display.inputter.element.selectionStart; + var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor); + return statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + }, + + cursor: function() { + return helpers._display.inputter.element.selectionStart; + }, + + current: function() { + return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name; + }, + + status: function() { + return helpers._display.requisition.getStatus().toString(); + }, + + outputState: function() { + var outputData = helpers._display.focusManager._shouldShowOutput(); + return outputData.visible + ':' + outputData.reason; + }, + + tooltipState: function() { + var tooltipData = helpers._display.focusManager._shouldShowTooltip(); + return tooltipData.visible + ':' + tooltipData.reason; + } +}; + +helpers._directToString = [ 'boolean', 'undefined', 'number' ]; + +helpers._createDebugCheck = function() { + var requisition = helpers._display.requisition; + var command = requisition.commandAssignment.value; + var input = helpers._actual.input(); + var padding = Array(input.length + 1).join(' '); + + var output = ''; + output += 'helpers.setInput(\'' + input + '\');\n'; + output += 'helpers.check({\n'; + output += ' input: \'' + input + '\',\n'; + output += ' hints: ' + padding + '\'' + helpers._actual.hints() + '\',\n'; + output += ' markup: \'' + helpers._actual.markup() + '\',\n'; + output += ' cursor: ' + helpers._actual.cursor() + ',\n'; + output += ' current: \'' + helpers._actual.current() + '\',\n'; + output += ' status: \'' + helpers._actual.status() + '\',\n'; + output += ' outputState: \'' + helpers._actual.outputState() + '\',\n'; + + if (command) { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\',\n'; + output += ' args: {\n'; + output += ' command: { name: \'' + command.name + '\' },\n'; + + requisition.getAssignments().forEach(function(assignment) { + output += ' ' + assignment.param.name + ': { '; + + if (typeof assignment.value === 'string') { + output += 'value: \'' + assignment.value + '\', '; + } + else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) { + output += 'value: ' + assignment.value + ', '; + } + else if (assignment.value === null) { + output += 'value: ' + assignment.value + ', '; + } + else { + output += '/*value:' + assignment.value + ',*/ '; + } + + output += 'arg: \'' + assignment.arg + '\', '; + output += 'status: \'' + assignment.getStatus().toString() + '\', '; + output += 'message: \'' + assignment.getMessage() + '\''; + output += ' },\n'; + }); + + output += ' }\n'; + } + else { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\'\n'; + } + output += '});'; + + return output; +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +helpers.setInput = function(typed, cursor) { + helpers._display.inputter.setInput(typed); + + if (cursor) { + helpers._display.inputter.setCursor({ start: cursor, end: cursor }); + } + + helpers._display.focusManager.onInputChange(); + + test.log('setInput("' + typed + '"' + (cursor == null ? '' : ', ' + cursor) + ')'); +}; + +/** + * Simulate focusing the input field + */ +helpers.focusInput = function() { + helpers._display.inputter.focus(); +}; + +/** + * Simulate pressing TAB in the input field + */ +helpers.pressTab = function() { + helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/); +}; + +/** + * Simulate pressing RETURN in the input field + */ +helpers.pressReturn = function() { + helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/); +}; + +/** + * Simulate pressing a key by keyCode in the input field + */ +helpers.pressKey = function(keyCode) { + var fakeEvent = { + keyCode: keyCode, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + helpers._display.inputter.onKeyDown(fakeEvent); + helpers._display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +helpers.check = function(checks) { + if ('input' in checks) { + test.is(helpers._actual.input(), checks.input, 'input'); + } + + if ('cursor' in checks) { + test.is(helpers._actual.cursor(), checks.cursor, 'cursor'); + } + + if ('current' in checks) { + test.is(helpers._actual.current(), checks.current, 'current'); + } + + if ('status' in checks) { + test.is(helpers._actual.status(), checks.status, 'status'); + } + + if ('markup' in checks) { + test.is(helpers._actual.markup(), checks.markup, 'markup'); + } + + if ('hints' in checks) { + test.is(helpers._actual.hints(), checks.hints, 'hints'); + } + + if ('tooltipState' in checks) { + test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState'); + } + + if ('outputState' in checks) { + test.is(helpers._actual.outputState(), checks.outputState, 'outputState'); + } + + if (checks.args != null) { + var requisition = helpers._display.requisition; + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown arg: ' + paramName); + return; + } + + if ('value' in check) { + test.is(assignment.value, + check.value, + 'arg.' + paramName + '.value'); + } + + if ('name' in check) { + test.is(assignment.value.name, + check.name, + 'arg.' + paramName + '.name'); + } + + if ('type' in check) { + test.is(assignment.arg.type, + check.type, + 'arg.' + paramName + '.type'); + } + + if ('arg' in check) { + test.is(assignment.arg.toString(), + check.arg, + 'arg.' + paramName + '.arg'); + } + + if ('status' in check) { + test.is(assignment.getStatus().toString(), + check.status, + 'arg.' + paramName + '.status'); + } + + if ('message' in check) { + test.is(assignment.getMessage(), + check.message, + 'arg.' + paramName + '.message'); + } + }); + } +}; + +/** + * Execute a command: + * + * helpers.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // Regex to test against textContent of output + * blankOutput: true, // Special checks when there is no output + * }); + */ +helpers.exec = function(tests) { + var requisition = helpers._display.requisition; + var inputter = helpers._display.inputter; + + tests = tests || {}; + + if (tests.typed) { + inputter.setInput(tests.typed); + } + + var typed = inputter.getInputState().typed; + var output = requisition.exec({ hidden: true }); + + test.is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + test.ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // test.ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + test.is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + var expectedArg = tests.args[arg]; + var actualArg = output.args[arg]; + + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + test.ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + test.is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (var i = 0; i < expectedArg.length; i++) { + test.is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + test.is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + }); + } + + if (!options.window.document.createElement) { + test.log('skipping output tests (missing doc.createElement) for ' + typed); + return; + } + + var div = options.window.document.createElement('div'); + output.toDom(div); + var displayed = div.textContent.trim(); + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + test.ok(false, "html output for " + typed + " against " + match.source); + console.log("Actual textContent"); + console.log(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + test.ok(false, "html for " + typed + " (textContent sent to info)"); + console.log("Actual textContent"); + console.log(displayed); + } + } +};
--- a/browser/devtools/styleeditor/test/Makefile.in +++ b/browser/devtools/styleeditor/test/Makefile.in @@ -23,17 +23,17 @@ include $(topsrcdir)/config/rules.mk browser_styleeditor_passedinsheet.js \ browser_styleeditor_pretty.js \ browser_styleeditor_readonly.js \ browser_styleeditor_reopen.js \ browser_styleeditor_sv_keynav.js \ browser_styleeditor_sv_resize.js \ four.html \ head.js \ - helper.js \ + helpers.js \ media.html \ media-small.css \ minified.html \ resources_inpage.jsi \ resources_inpage1.css \ resources_inpage2.css \ simple.css \ simple.css.gz \
--- a/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js @@ -8,105 +8,146 @@ const TEST_URI = "http://example.com/bro function test() { DeveloperToolbarTest.test(TEST_URI, [ testEditStatus ]); // Bug 759853 // testEditExec } function testEditStatus(browser, tab) { - DeveloperToolbarTest.checkInputStatus({ - typed: "edit", - markup: "VVVV", - status: "ERROR", - emptyParameters: [ " <resource>", " [line]" ], + helpers.setInput('edit'); + helpers.check({ + input: 'edit', + hints: ' <resource> [line]', + markup: 'VVVV', + status: 'ERROR', + args: { + resource: { status: 'INCOMPLETE' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit i", - markup: "VVVVVI", - status: "ERROR", - directTabText: "nline-css", - emptyParameters: [ " [line]" ], + helpers.setInput('edit i'); + helpers.check({ + input: 'edit i', + hints: 'nline-css [line]', + markup: 'VVVVVI', + status: 'ERROR', + args: { + resource: { arg: ' i', status: 'INCOMPLETE' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit c", - markup: "VVVVVI", - status: "ERROR", - directTabText: "ss#style2", - emptyParameters: [ " [line]" ], + helpers.setInput('edit c'); + helpers.check({ + input: 'edit c', + hints: 'ss#style2 [line]', + markup: 'VVVVVI', + status: 'ERROR', + args: { + resource: { arg: ' c', status: 'INCOMPLETE' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit http", - markup: "VVVVVIIII", - status: "ERROR", - directTabText: "://example.com/browser/browser/devtools/styleeditor/test/resources_inpage1.css", - arrowTabText: "", - emptyParameters: [ " [line]" ], + helpers.setInput('edit http'); + helpers.check({ + input: 'edit http', + hints: '://example.com/browser/browser/devtools/styleeditor/test/resources_inpage1.css [line]', + markup: 'VVVVVIIII', + status: 'ERROR', + args: { + resource: { arg: ' http', status: 'INCOMPLETE', message: '' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit page1", - markup: "VVVVVIIIII", - status: "ERROR", - directTabText: "", - arrowTabText: "http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage1.css", - emptyParameters: [ " [line]" ], + helpers.setInput('edit page1'); + helpers.check({ + input: 'edit page1', + hints: ' [line] -> http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage1.css', + markup: 'VVVVVIIIII', + status: 'ERROR', + args: { + resource: { arg: ' page1', status: 'INCOMPLETE', message: '' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit page2", - markup: "VVVVVIIIII", - status: "ERROR", - directTabText: "", - arrowTabText: "http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage2.css", - emptyParameters: [ " [line]" ], + helpers.setInput('edit page2'); + helpers.check({ + input: 'edit page2', + hints: ' [line] -> http://example.com/browser/browser/devtools/styleeditor/test/resources_inpage2.css', + markup: 'VVVVVIIIII', + status: 'ERROR', + args: { + resource: { arg: ' page2', status: 'INCOMPLETE', message: '' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit stylez", - markup: "VVVVVEEEEEE", - status: "ERROR", - directTabText: "", - arrowTabText: "", - emptyParameters: [ " [line]" ], + helpers.setInput('edit stylez'); + helpers.check({ + input: 'edit stylez', + hints: ' [line]', + markup: 'VVVVVEEEEEE', + status: 'ERROR', + args: { + resource: { arg: ' stylez', status: 'ERROR', message: 'Can\'t use \'stylez\'.' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit css#style2", - markup: "VVVVVVVVVVVVVVV", - status: "VALID", - directTabText: "", - emptyParameters: [ " [line]" ], + helpers.setInput('edit css#style2'); + helpers.check({ + input: 'edit css#style2', + hints: ' [line]', + markup: 'VVVVVVVVVVVVVVV', + status: 'VALID', + args: { + resource: { arg: ' css#style2', status: 'VALID', message: '' }, + line: { status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit css#style2 5", - markup: "VVVVVVVVVVVVVVVVV", - status: "VALID", - directTabText: "", - emptyParameters: [ ], + helpers.setInput('edit css#style2 5'); + helpers.check({ + input: 'edit css#style2 5', + hints: '', + markup: 'VVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + resource: { arg: ' css#style2', status: 'VALID', message: '' }, + line: { value: 5, arg: ' 5', status: 'VALID' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit css#style2 0", - markup: "VVVVVVVVVVVVVVVVE", - status: "ERROR", - directTabText: "", - emptyParameters: [ ], + helpers.setInput('edit css#style2 0'); + helpers.check({ + input: 'edit css#style2 0', + hints: '', + markup: 'VVVVVVVVVVVVVVVVE', + status: 'ERROR', + args: { + resource: { arg: ' css#style2', status: 'VALID', message: '' }, + line: { arg: ' 0', status: 'ERROR', message: '0 is smaller than minimum allowed: 1.' }, + } }); - DeveloperToolbarTest.checkInputStatus({ - typed: "edit css#style2 -1", - markup: "VVVVVVVVVVVVVVVVEE", - status: "ERROR", - directTabText: "", - emptyParameters: [ ], + helpers.setInput('edit css#style2 -1'); + helpers.check({ + input: 'edit css#style2 -1', + hints: '', + markup: 'VVVVVVVVVVVVVVVVEE', + status: 'ERROR', + args: { + resource: { arg: ' css#style2', status: 'VALID', message: '' }, + line: { arg: ' -1', status: 'ERROR', message: '-1 is smaller than minimum allowed: 1.' }, + } }); } var windowListener = { onOpenWindow: function(win) { // Wait for the window to finish loading let win = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
--- a/browser/devtools/styleeditor/test/head.js +++ b/browser/devtools/styleeditor/test/head.js @@ -4,17 +4,17 @@ const TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/styleeditor/test/"; const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/styleeditor/test/"; const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/styleeditor/test/"; let gChromeWindow; //StyleEditorChrome window // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); -Services.scriptloader.loadSubScript(testDir + "/helper.js", this); +Services.scriptloader.loadSubScript(testDir + "/helpers.js", this); function cleanup() { if (gChromeWindow) { gChromeWindow.close(); gChromeWindow = null; } while (gBrowser.tabs.length > 1) {
rename from browser/devtools/styleeditor/test/helper.js rename to browser/devtools/styleeditor/test/helpers.js --- a/browser/devtools/styleeditor/test/helper.js +++ b/browser/devtools/styleeditor/test/helpers.js @@ -26,16 +26,43 @@ * * * * FOR A LONG TIME. * */ +/* + * Use as a JSM + * ------------ + * helpers._createDebugCheck() and maybe other functions in this file can be + * useful at runtime, so it is possible to use helpers.js as a JSM. + * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to + * DeveloperToolbar.jsm the following: + * + * XPCOMUtils.defineLazyModuleGetter(this, "helpers", + * "resource:///modules/devtools/helpers.jsm"); + * + * At the bottom of DeveloperToolbar.prototype._onload add this: + * + * var options = { display: this.display }; + * this._input.onkeypress = function(ev) { + * helpers.setup(options); + * dump(helpers._createDebugCheck() + '\n\n'); + * }; + * + * Now GCLI will emit output on every keypress that both explains the state + * of GCLI and can be run as a test case. + */ + +var EXPORTED_SYMBOLS = [ 'helpers' ]; + +var test = { }; + /** * Various functions for testing DeveloperToolbar. * Parts of this code exist in: * - browser/devtools/commandline/test/head.js * - browser/devtools/shared/test/head.js */ let DeveloperToolbarTest = { }; @@ -158,16 +185,19 @@ DeveloperToolbarTest.checkInputStatus = if (actualParams.length === checks.emptyParameters.length) { for (var i = 0; i < actualParams.length; i++) { is(actualParams[i].replace(/\u00a0/g, ' '), checks.emptyParameters[i], 'emptyParameters[' + i + ']'); } } + else { + info('Expected: [ \"' + actualParams.join('", "') + '" ]'); + } } if (checks.directTabText) { is(actual.directTabText, checks.directTabText, 'directTabText'); } @@ -385,16 +415,20 @@ DeveloperToolbarTest.test = function DTT let tab = gBrowser.selectedTab; let browser = gBrowser.getBrowserForTab(tab); var onTabLoad = function() { browser.removeEventListener("load", onTabLoad, true); DeveloperToolbarTest.show(function() { + if (helpers) { + helpers.setup({ display: DeveloperToolbar.display }); + } + if (Array.isArray(target)) { try { target.forEach(function(func) { func(browser, tab); }) } finally { DeveloperToolbarTest._checkFinish(); @@ -414,22 +448,24 @@ DeveloperToolbarTest.test = function DTT } browser.addEventListener("load", onTabLoad, true); }; DeveloperToolbarTest._outstanding = []; DeveloperToolbarTest._checkFinish = function() { + info('_checkFinish. ' + DeveloperToolbarTest._outstanding.length + ' outstanding'); if (DeveloperToolbarTest._outstanding.length == 0) { DeveloperToolbarTest._finish(); } } DeveloperToolbarTest._finish = function() { + info('Finish'); DeveloperToolbarTest.closeAllTabs(); finish(); } DeveloperToolbarTest.checkCalled = function(aFunc, aScope) { var todo = function() { var reply = aFunc.apply(aScope, arguments); DeveloperToolbarTest._outstanding = DeveloperToolbarTest._outstanding.filter(function(aJob) { @@ -452,8 +488,394 @@ DeveloperToolbarTest.checkNotCalled = fu /** * */ DeveloperToolbarTest.closeAllTabs = function() { while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } }; + +/////////////////////////////////////////////////////////////////////////////// + +var helpers = {}; + +helpers._display = undefined; + +helpers.setup = function(options) { + helpers._display = options.display; + if (typeof ok !== 'undefined') { + test.ok = ok; + test.is = is; + test.log = info; + } +}; + +helpers.shutdown = function(options) { + helpers._display = undefined; +}; + +/** + * Various functions to return the actual state of the command line + */ +helpers._actual = { + input: function() { + return helpers._display.inputter.element.value; + }, + + hints: function() { + var templateData = helpers._display.completer._getCompleterTemplateData(); + var actualHints = templateData.directTabText + + templateData.emptyParameters.join('') + + templateData.arrowTabText; + return actualHints.replace(/\u00a0/g, ' ') + .replace(/\u21E5/, '->') + .replace(/ $/, ''); + }, + + markup: function() { + var cursor = helpers._display.inputter.element.selectionStart; + var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor); + return statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + }, + + cursor: function() { + return helpers._display.inputter.element.selectionStart; + }, + + current: function() { + return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name; + }, + + status: function() { + return helpers._display.requisition.getStatus().toString(); + }, + + outputState: function() { + var outputData = helpers._display.focusManager._shouldShowOutput(); + return outputData.visible + ':' + outputData.reason; + }, + + tooltipState: function() { + var tooltipData = helpers._display.focusManager._shouldShowTooltip(); + return tooltipData.visible + ':' + tooltipData.reason; + } +}; + +helpers._directToString = [ 'boolean', 'undefined', 'number' ]; + +helpers._createDebugCheck = function() { + var requisition = helpers._display.requisition; + var command = requisition.commandAssignment.value; + var input = helpers._actual.input(); + var padding = Array(input.length + 1).join(' '); + + var output = ''; + output += 'helpers.setInput(\'' + input + '\');\n'; + output += 'helpers.check({\n'; + output += ' input: \'' + input + '\',\n'; + output += ' hints: ' + padding + '\'' + helpers._actual.hints() + '\',\n'; + output += ' markup: \'' + helpers._actual.markup() + '\',\n'; + output += ' cursor: ' + helpers._actual.cursor() + ',\n'; + output += ' current: \'' + helpers._actual.current() + '\',\n'; + output += ' status: \'' + helpers._actual.status() + '\',\n'; + output += ' outputState: \'' + helpers._actual.outputState() + '\',\n'; + + if (command) { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\',\n'; + output += ' args: {\n'; + output += ' command: { name: \'' + command.name + '\' },\n'; + + requisition.getAssignments().forEach(function(assignment) { + output += ' ' + assignment.param.name + ': { '; + + if (typeof assignment.value === 'string') { + output += 'value: \'' + assignment.value + '\', '; + } + else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) { + output += 'value: ' + assignment.value + ', '; + } + else if (assignment.value === null) { + output += 'value: ' + assignment.value + ', '; + } + else { + output += '/*value:' + assignment.value + ',*/ '; + } + + output += 'arg: \'' + assignment.arg + '\', '; + output += 'status: \'' + assignment.getStatus().toString() + '\', '; + output += 'message: \'' + assignment.getMessage() + '\''; + output += ' },\n'; + }); + + output += ' }\n'; + } + else { + output += ' tooltipState: \'' + helpers._actual.tooltipState() + '\'\n'; + } + output += '});'; + + return output; +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +helpers.setInput = function(typed, cursor) { + helpers._display.inputter.setInput(typed); + + if (cursor) { + helpers._display.inputter.setCursor({ start: cursor, end: cursor }); + } + + helpers._display.focusManager.onInputChange(); + + test.log('setInput("' + typed + '"' + (cursor == null ? '' : ', ' + cursor) + ')'); +}; + +/** + * Simulate focusing the input field + */ +helpers.focusInput = function() { + helpers._display.inputter.focus(); +}; + +/** + * Simulate pressing TAB in the input field + */ +helpers.pressTab = function() { + helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/); +}; + +/** + * Simulate pressing RETURN in the input field + */ +helpers.pressReturn = function() { + helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/); +}; + +/** + * Simulate pressing a key by keyCode in the input field + */ +helpers.pressKey = function(keyCode) { + var fakeEvent = { + keyCode: keyCode, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + helpers._display.inputter.onKeyDown(fakeEvent); + helpers._display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * hints: The hint text, i.e. a concatenation of the directTabText, the + * emptyParameters and the arrowTabText. The text as inserted into the UI + * will include NBSP and Unicode RARR characters, these should be + * represented using normal space and '->' for the arrow + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +helpers.check = function(checks) { + if ('input' in checks) { + test.is(helpers._actual.input(), checks.input, 'input'); + } + + if ('cursor' in checks) { + test.is(helpers._actual.cursor(), checks.cursor, 'cursor'); + } + + if ('current' in checks) { + test.is(helpers._actual.current(), checks.current, 'current'); + } + + if ('status' in checks) { + test.is(helpers._actual.status(), checks.status, 'status'); + } + + if ('markup' in checks) { + test.is(helpers._actual.markup(), checks.markup, 'markup'); + } + + if ('hints' in checks) { + test.is(helpers._actual.hints(), checks.hints, 'hints'); + } + + if ('tooltipState' in checks) { + test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState'); + } + + if ('outputState' in checks) { + test.is(helpers._actual.outputState(), checks.outputState, 'outputState'); + } + + if (checks.args != null) { + var requisition = helpers._display.requisition; + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown arg: ' + paramName); + return; + } + + if ('value' in check) { + test.is(assignment.value, + check.value, + 'arg.' + paramName + '.value'); + } + + if ('name' in check) { + test.is(assignment.value.name, + check.name, + 'arg.' + paramName + '.name'); + } + + if ('type' in check) { + test.is(assignment.arg.type, + check.type, + 'arg.' + paramName + '.type'); + } + + if ('arg' in check) { + test.is(assignment.arg.toString(), + check.arg, + 'arg.' + paramName + '.arg'); + } + + if ('status' in check) { + test.is(assignment.getStatus().toString(), + check.status, + 'arg.' + paramName + '.status'); + } + + if ('message' in check) { + test.is(assignment.getMessage(), + check.message, + 'arg.' + paramName + '.message'); + } + }); + } +}; + +/** + * Execute a command: + * + * helpers.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // Regex to test against textContent of output + * blankOutput: true, // Special checks when there is no output + * }); + */ +helpers.exec = function(tests) { + var requisition = helpers._display.requisition; + var inputter = helpers._display.inputter; + + tests = tests || {}; + + if (tests.typed) { + inputter.setInput(tests.typed); + } + + var typed = inputter.getInputState().typed; + var output = requisition.exec({ hidden: true }); + + test.is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + test.ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // test.ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + test.is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); + + Object.keys(output.args).forEach(function(arg) { + var expectedArg = tests.args[arg]; + var actualArg = output.args[arg]; + + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + test.ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + test.is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (var i = 0; i < expectedArg.length; i++) { + test.is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + test.is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + }); + } + + if (!options.window.document.createElement) { + test.log('skipping output tests (missing doc.createElement) for ' + typed); + return; + } + + var div = options.window.document.createElement('div'); + output.toDom(div); + var displayed = div.textContent.trim(); + + if (tests.outputMatch) { + var doTest = function(match, against) { + if (!match.test(against)) { + test.ok(false, "html output for " + typed + " against " + match.source); + console.log("Actual textContent"); + console.log(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); + } + else { + doTest(tests.outputMatch, displayed); + } + } + + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + test.ok(false, "html for " + typed + " (textContent sent to info)"); + console.log("Actual textContent"); + console.log(displayed); + } + } +};