Bug 773565 - GCLI Autocomplete goes wild when boolean params are used in a group; r=dcamp
authorJoe Walker <jwalker@mozilla.com>
Fri, 24 Aug 2012 16:04:45 +0100
changeset 105425 c80d0e010be310c2770e6668a89ee5cce1e97f3d
parent 105424 a275a3c32e714c1bc5e8e886e60b15804f4dd5c5
child 105426 73b1af99b72db8051f53b8fff859637cc980f740
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersdcamp
bugs773565
milestone17.0a1
Bug 773565 - GCLI Autocomplete goes wild when boolean params are used in a group; r=dcamp
browser/devtools/commandline/CmdJsb.jsm
browser/devtools/commandline/CmdRestart.jsm
browser/devtools/commandline/CmdScreenshot.jsm
browser/devtools/commandline/gcli.jsm
browser/devtools/commandline/test/Makefile.in
browser/devtools/commandline/test/browser_cmd_addon.js
browser/devtools/commandline/test/browser_cmd_calllog.js
browser/devtools/commandline/test/browser_cmd_cookie.js
browser/devtools/commandline/test/browser_cmd_jsb.js
browser/devtools/commandline/test/browser_cmd_pagemod_export.js
browser/devtools/commandline/test/browser_cmd_pref.js
browser/devtools/commandline/test/browser_cmd_restart.js
browser/devtools/commandline/test/browser_dbg_cmd_break.js
browser/devtools/commandline/test/browser_gcli_web.js
browser/devtools/commandline/test/head.js
browser/devtools/commandline/test/helper.js
browser/devtools/commandline/test/helpers.js
browser/devtools/highlighter/test/Makefile.in
browser/devtools/highlighter/test/browser_inspector_cmd_inspect.js
browser/devtools/highlighter/test/head.js
browser/devtools/highlighter/test/helper.js
browser/devtools/highlighter/test/helpers.js
browser/devtools/responsivedesign/test/Makefile.in
browser/devtools/responsivedesign/test/browser_responsive_cmd.js
browser/devtools/responsivedesign/test/head.js
browser/devtools/responsivedesign/test/helper.js
browser/devtools/responsivedesign/test/helpers.js
browser/devtools/shared/test/Makefile.in
browser/devtools/shared/test/head.js
browser/devtools/shared/test/helper.js
browser/devtools/shared/test/helpers.js
browser/devtools/styleeditor/test/Makefile.in
browser/devtools/styleeditor/test/browser_styleeditor_cmd_edit.js
browser/devtools/styleeditor/test/head.js
browser/devtools/styleeditor/test/helper.js
browser/devtools/styleeditor/test/helpers.js
--- 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 &nbsp;&rarr; 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 &nbsp; 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. &nbsp;
     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. &nbsp;
-    }
-
-    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);
+    }
+  }
+};