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 11:05:19 +0100
changeset 103440 35e5ee61e193771a906a39c7d21e66651cf03ae0
parent 103439 7bf846af58d35674c1b02f8dbfa260f6012a78d5
child 103441 f4596ef17eed20290eb9b62fb2089272544d4ac9
push id13991
push userryanvm@gmail.com
push dateSun, 26 Aug 2012 02:29:03 +0000
treeherdermozilla-inbound@c4f20a024113 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs773565
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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/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,112 @@
 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'
+  });
+
+  helpers.setInput('inspect h1');
+  helpers.check({
+    input:  'inspect h1',
+    hints:            '',
+    markup: 'VVVVVVVVII',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect",
-    emptyParameters: [ " <node>" ],
-    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 h1",
-    status: "ERROR"
+  helpers.setInput('inspect div');
+  helpers.check({
+    input:  'inspect div',
+    hints:             '',
+    markup: 'VVVVVVVVEEE',
+    status: 'ERROR',
+    args: {
+      node: { message: 'Too many matches (10)' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect span",
-    status: "ERROR"
+  helpers.setInput('inspect .someclas');
+  helpers.check({
+    input:  'inspect .someclas',
+    hints:                   '',
+    markup: 'VVVVVVVVIIIIIIIII',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect div",
-    status: "VALID"
+  helpers.setInput('inspect .someclass');
+  helpers.check({
+    input:  'inspect .someclass',
+    hints:                    '',
+    markup: 'VVVVVVVVIIIIIIIIII',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect .someclass",
-    status: "VALID"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect #someid",
-    status: "VALID"
+  helpers.setInput('inspect #someid');
+  helpers.check({
+    input:  'inspect #someid',
+    hints:                 '',
+    markup: 'VVVVVVVVEEEEEEE',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect button[disabled]",
-    status: "VALID"
+  helpers.setInput('inspect button[disabled]');
+  helpers.check({
+    input:  'inspect button[disabled]',
+    hints:                          '',
+    markup: 'VVVVVVVVIIIIIIIIIIIIIIII',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect p>strong",
-    status: "VALID"
+  helpers.setInput('inspect p>strong');
+  helpers.check({
+    input:  'inspect p>strong',
+    hints:                  '',
+    markup: 'VVVVVVVVEEEEEEEE',
+    status: 'ERROR',
+    args: {
+      node: { message: 'No matches' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "inspect :root",
-    status: "VALID"
+  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/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);
+    }
+  }
+};