Bug 773227 - GCLI should open a help menu on enter for non-VALID (like pressing F1); r=dcamp
authorJoe Walker <jwalker@mozilla.com>
Sat, 25 Aug 2012 21:18:56 +0100
changeset 103465 4d6471c1cdd925abdbffc40019df0bba6de833d8
parent 103464 e11af99c55665e44df7318661f524742a0b8952b
child 103466 bc23ba35391382a3de4dd325ad6baaaa88292ec3
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
bugs773227
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 773227 - GCLI should open a help menu on enter for non-VALID (like pressing F1); r=dcamp
browser/devtools/commandline/gcli.jsm
browser/devtools/commandline/test/browser_gcli_web.js
--- a/browser/devtools/commandline/gcli.jsm
+++ b/browser/devtools/commandline/gcli.jsm
@@ -101,20 +101,18 @@ define('gcli/index', ['require', 'export
   require('gcli/ui/focus').startup();
   require('gcli/ui/fields/basic').startup();
   require('gcli/ui/fields/javascript').startup();
   require('gcli/ui/fields/selection').startup();
 
   require('gcli/commands/help').startup();
   require('gcli/commands/pref').startup();
 
-
   var Cc = Components.classes;
   var Ci = Components.interfaces;
-  var Cu = Components.utils;
   var prefSvc = "@mozilla.org/preferences-service;1";
   var prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
   var prefBranch = prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
 
   // The API for use by command authors
   exports.addCommand = require('gcli/canon').addCommand;
   exports.removeCommand = require('gcli/canon').removeCommand;
   exports.lookup = mozl10n.lookup;
@@ -137,16 +135,17 @@ define('gcli/index', ['require', 'export
   exports.createDisplay = function(opts) {
     var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay;
     return new FFDisplay(opts);
   };
 
   exports.hiddenByChromePref = function() {
     return !prefBranch.prefHasUserValue("devtools.chrome.enabled");
   };
+
 });
 /*
  * 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
  *
@@ -4017,52 +4016,64 @@ exports.JavascriptType = JavascriptType;
  *
  * 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/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types'], function(require, exports, module) {
+define('gcli/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types', 'gcli/argument'], function(require, exports, module) {
 
 
 var host = require('gcli/host');
 var l10n = require('gcli/l10n');
 var types = require('gcli/types');
 var Type = require('gcli/types').Type;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
+var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
   types.registerType(NodeType);
+  types.registerType(NodeListType);
 };
 
 exports.shutdown = function() {
   types.unregisterType(NodeType);
+  types.unregisterType(NodeListType);
 };
 
 /**
  * The object against which we complete, which is usually 'window' if it exists
  * but could be something else in non-web-content environments.
  */
 var doc;
 if (typeof document !== 'undefined') {
   doc = document;
 }
 
 /**
+ * For testing only.
+ * The fake empty NodeList used when there are no matches, we replace this with
+ * something that looks better as soon as we have a document, so not only
+ * should you not use this, but you shouldn't cache it either.
+ */
+exports._empty = [];
+
+/**
  * Setter for the document that contains the nodes we're matching
  */
 exports.setDocument = function(document) {
   doc = document;
+  exports._empty = doc.querySelectorAll('x>:root');
 };
 
 /**
  * Undo the effects of setDocument()
  */
 exports.unsetDocument = function() {
   doc = undefined;
 };
@@ -4123,16 +4134,76 @@ NodeType.prototype.parse = function(arg)
 
   return new Conversion(undefined, arg, Status.ERROR,
           l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]));
 };
 
 NodeType.prototype.name = 'node';
 
 
+
+/**
+ * A CSS expression that refers to a node list.
+ *
+ * The 'allowEmpty' option ensures that we do not complain if the entered CSS
+ * selector is valid, but does not match any nodes. There is some overlap
+ * between this option and 'defaultValue'. What the user wants, in most cases,
+ * would be to use 'defaultText' (i.e. what is typed rather than the value that
+ * it represents). However this isn't a concept that exists yet and should
+ * probably be a part of GCLI if/when it does.
+ * All NodeListTypes have an automatic defaultValue of an empty NodeList so
+ * they can easily be used in named parameters.
+ */
+function NodeListType(typeSpec) {
+  if ('allowEmpty' in typeSpec && typeof typeSpec.allowEmpty !== 'boolean') {
+    throw new Error('Legal values for allowEmpty are [true|false]');
+  }
+
+  this.allowEmpty = typeSpec.allowEmpty;
+}
+
+NodeListType.prototype = Object.create(Type.prototype);
+
+NodeListType.prototype.getBlank = function() {
+  return new Conversion(exports._empty, new BlankArgument(), Status.VALID);
+};
+
+NodeListType.prototype.stringify = function(value) {
+  if (value == null) {
+    return '';
+  }
+  return value.__gcliQuery || 'Error';
+};
+
+NodeListType.prototype.parse = function(arg) {
+  if (arg.text === '') {
+    return new Conversion(undefined, arg, Status.INCOMPLETE);
+  }
+
+  var nodes;
+  try {
+    nodes = doc.querySelectorAll(arg.text);
+  }
+  catch (ex) {
+    return new Conversion(undefined, arg, Status.ERROR,
+            l10n.lookup('nodeParseSyntax'));
+  }
+
+  if (nodes.length === 0 && !this.allowEmpty) {
+    return new Conversion(undefined, arg, Status.INCOMPLETE,
+        l10n.lookup('nodeParseNone'));
+  }
+
+  host.flashNodes(nodes, false);
+  return new Conversion(nodes, arg, Status.VALID, '');
+};
+
+NodeListType.prototype.name = 'nodelist';
+
+
 });
 /*
  * 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
  *
@@ -5779,54 +5850,22 @@ Requisition.prototype.complete = functio
 
   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
+ * @param assignment The assignment to which to append the 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);
-    }
+  var arg = assignment.conversion.arg.beget({ suffixSpace: true });
+  if (arg !== assignment.conversion.arg) {
+    this.setAssignment(assignment, arg);
   }
 };
 
 /**
  * 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);
@@ -6160,17 +6199,17 @@ Requisition.prototype.exec = function(in
   });
 
   this.commandOutputManager.onOutput({ output: output });
 
   try {
     var context = exports.createExecutionContext(this);
     var reply = command.exec(args, context);
 
-    if (reply != null && reply.isPromise) {
+    if (reply != null && typeof reply.then === 'function') {
       reply.then(
           function(data) { output.complete(data); },
           function(error) { output.error = true; output.complete(error); });
 
       output.promise = reply;
       // Add progress to our promise and add a handler for it here
       // See bug 659300
     }
@@ -6987,16 +7026,21 @@ FocusManager.prototype.addMonitoredEleme
     element: element,
     where: where,
     onFocus: function() { this._reportFocus(where); }.bind(this),
     onBlur: function() { this._reportBlur(where); }.bind(this)
   };
 
   element.addEventListener('focus', monitor.onFocus, true);
   element.addEventListener('blur', monitor.onBlur, true);
+
+  if (this._document.activeElement === element) {
+    this._reportFocus(where);
+  }
+
   this._monitoredElements.push(monitor);
 };
 
 /**
  * Undo the effects of addMonitoredElement()
  * @param element The element to stop tracking
  * @param where Optional source string for debugging only
  */
@@ -7104,19 +7148,19 @@ FocusManager.prototype._reportBlur = fun
  * The setting has changed
  */
 FocusManager.prototype._eagerHelperChanged = function() {
   this._checkShow();
 };
 
 /**
  * The inputter tells us about keyboard events so we can decide to delay
- * showing the tooltip element, (or if the keypress is F1, show it now)
- */
-FocusManager.prototype.onInputChange = function(ev) {
+ * showing the tooltip element
+ */
+FocusManager.prototype.onInputChange = function() {
   this._recentOutput = false;
   this._checkShow();
 };
 
 /**
  * Generally called for something like a F1 key press, when the user explicitly
  * wants help
  */
@@ -7200,25 +7244,25 @@ FocusManager.prototype._checkShow = func
 };
 
 /**
  * Calculate if we should be showing or hidden taking into account all the
  * available inputs
  */
 FocusManager.prototype._shouldShowTooltip = function() {
   if (!this._hasFocus) {
-    return { visible: false, reason: '!hasFocus' };
+    return { visible: false, reason: 'notHasFocus' };
   }
 
   if (eagerHelper.value === Eagerness.NEVER) {
-    return { visible: false, reason: 'eagerHelper !== NEVER' };
+    return { visible: false, reason: 'eagerHelperNever' };
   }
 
   if (eagerHelper.value === Eagerness.ALWAYS) {
-    return { visible: true, reason: 'eagerHelper !== ALWAYS' };
+    return { visible: true, reason: 'eagerHelperAlways' };
   }
 
   if (this._isError) {
     return { visible: true, reason: 'isError' };
   }
 
   if (this._helpRequested) {
     return { visible: true, reason: 'helpRequested' };
@@ -7232,17 +7276,17 @@ FocusManager.prototype._shouldShowToolti
 };
 
 /**
  * Calculate if we should be showing or hidden taking into account all the
  * available inputs
  */
 FocusManager.prototype._shouldShowOutput = function() {
   if (!this._hasFocus) {
-    return { visible: false, reason: '!hasFocus' };
+    return { visible: false, reason: 'notHasFocus' };
   }
 
   if (this._recentOutput) {
     return { visible: true, reason: 'recentOutput' };
   }
 
   return { visible: false, reason: 'default' };
 };
@@ -8212,27 +8256,31 @@ Menu.prototype.setChoiceIndex = function
   }
 
   nodes.item(choice).classList.add('gcli-menu-highlight');
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if an item was 'clicked', false otherwise
  */
 Menu.prototype.selectChoice = function() {
   var selected = this.element.querySelector('.gcli-menu-highlight .gcli-menu-name');
-  if (selected) {
-    var name = selected.innerHTML;
-    var arg = new Argument(name);
-    arg.suffix = ' ';
-
-    var conversion = this.type.parse(arg);
-    this.onItemClick({ conversion: conversion });
-  }
+  if (!selected) {
+    return false;
+  }
+
+  var name = selected.innerHTML;
+  var arg = new Argument(name);
+  arg.suffix = ' ';
+
+  var conversion = this.type.parse(arg);
+  this.onItemClick({ conversion: conversion });
+  return true;
 };
 
 /**
  * Hide the menu
  */
 Menu.prototype.hide = function() {
   this.element.style.display = 'none';
 };
@@ -8452,19 +8500,20 @@ SelectionTooltipField.prototype.getConve
  */
 SelectionTooltipField.prototype.setChoiceIndex = function(choice) {
   this.menu.setChoiceIndex(choice);
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if an item was 'clicked', false otherwise
  */
 SelectionTooltipField.prototype.selectChoice = function() {
-  this.menu.selectChoice();
+  return this.menu.selectChoice();
 };
 
 Object.defineProperty(SelectionTooltipField.prototype, 'isImportant', {
   get: function() {
     return this.type.name !== 'command';
   },
   enumerable: true
 });
@@ -9487,17 +9536,17 @@ Inputter.prototype.onKeyDown = function(
   if (ev.keyCode === KeyEvent.DOM_VK_F1 ||
       ev.keyCode === KeyEvent.DOM_VK_ESCAPE ||
       ev.keyCode === KeyEvent.DOM_VK_UP ||
       ev.keyCode === KeyEvent.DOM_VK_DOWN) {
     return;
   }
 
   if (this.focusManager) {
-    this.focusManager.onInputChange(ev);
+    this.focusManager.onInputChange();
   }
 
   if (ev.keyCode === KeyEvent.DOM_VK_TAB) {
     this.lastTabDownAt = 0;
     if (!ev.shiftKey) {
       ev.preventDefault();
       // Record the timestamp of this TAB down so onKeyUp can distinguish
       // focus from TAB in the CLI.
@@ -9538,17 +9587,17 @@ Inputter.prototype.onKeyUp = function(ev
     }
     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.requisition.increment(assignment);
         // See notes on focusManager.onInputChange in onKeyDown
         if (this.focusManager) {
-          this.focusManager.onInputChange(ev);
+          this.focusManager.onInputChange();
         }
       }
       else {
         this.changeChoice(-1);
       }
     }
     return;
   }
@@ -9562,17 +9611,17 @@ Inputter.prototype.onKeyUp = function(ev
       this.requisition.update(this.history.forward());
     }
     else {
       // See notes above for the UP key
       if (this.assignment.getStatus() === Status.VALID) {
         this.requisition.decrement(assignment);
         // See notes on focusManager.onInputChange in onKeyDown
         if (this.focusManager) {
-          this.focusManager.onInputChange(ev);
+          this.focusManager.onInputChange();
         }
       }
       else {
         this.changeChoice(+1);
       }
     }
     return;
   }
@@ -9584,20 +9633,19 @@ Inputter.prototype.onKeyUp = function(ev
     if (worst === Status.VALID) {
       this._scrollingThroughHistory = false;
       this.history.add(this.element.value);
       this.requisition.exec();
     }
     else {
       // If we can't execute the command, but there is a menu choice to use
       // then use it.
-      this.tooltip.selectChoice();
-
-      // See bug 664135 - On pressing return with an invalid input, GCLI
-      // should select the incorrect part of the input for an easy fix
+      if (!this.tooltip.selectChoice()) {
+        this.focusManager.setError(true);
+      }
     }
 
     this._choice = null;
     return;
   }
 
   if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) {
     // Being able to complete 'nothing' is OK if there is some context, but
@@ -10232,21 +10280,23 @@ Tooltip.prototype.choiceChanged = functi
     var choice = this.assignment.conversion.constrainPredictionIndex(ev.choice);
     this.field.setChoiceIndex(choice);
   }
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if there was a selection to use, false otherwise
  */
 Tooltip.prototype.selectChoice = function(ev) {
   if (this.field && this.field.selectChoice) {
-    this.field.selectChoice();
-  }
+    return this.field.selectChoice();
+  }
+  return false;
 };
 
 /**
  * Called by the onFieldChange event on the current Field
  */
 Tooltip.prototype.fieldChanged = function(ev) {
   this.assignment.setConversion(ev.conversion);
 
--- a/browser/devtools/commandline/test/browser_gcli_web.js
+++ b/browser/devtools/commandline/test/browser_gcli_web.js
@@ -150,21 +150,53 @@ define('gclitest/index', ['require', 'ex
       settings.setDefaults(options.settings);
     }
 
     window.display = new Display(options);
     var requisition = window.display.requisition;
 
     // setTimeout keeps stack traces clear of RequireJS frames
     window.setTimeout(function() {
-      exports.run({
+      var options = {
         window: window,
         display: window.display,
         hideExec: true
-      });
+      };
+      exports.run(options);
+
+      window.createDebugCheck = function() {
+        require([ 'gclitest/helpers' ], function(helpers) {
+          helpers.setup(options);
+          console.log(helpers._createDebugCheck());
+          helpers.shutdown(options);
+        });
+      };
+
+      window.summaryJson = function() {
+        var args = [ 'Requisition: ' ];
+        var summary = display.requisition._summaryJson;
+        Object.keys(summary).forEach(function(name) {
+          args.push(' ' + name + '=');
+          args.push(summary[name]);
+        });
+        console.log.apply(console, args);
+
+        console.log('Focus: ' +
+                    'tooltip=', display.focusManager._shouldShowTooltip(),
+                    'output=', display.focusManager._shouldShowOutput());
+      };
+
+      document.addEventListener('keyup', function(ev) {
+        if (ev.keyCode === 113 /*F2*/) {
+          window.createDebugCheck();
+        }
+        if (ev.keyCode === 115 /*F4*/) {
+          window.summaryJson();
+        }
+      }, true);
 
       window.testCommands = function() {
         require([ 'gclitest/mockCommands' ], function(mockCommands) {
           mockCommands.setup();
         });
       };
       window.testCommands();
     }, 10);
@@ -192,38 +224,40 @@ define('gclitest/index', ['require', 'ex
  *
  * 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('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testMenu', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], function(require, exports, module) {
+define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testFocus', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testMenu', 'gclitest/testNode', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], function(require, exports, module) {
 
   // We need to make sure GCLI is initialized before we begin testing it
   require('gcli/index');
 
   var examiner = require('test/examiner');
 
   // It's tempting to want to unify these strings and make addSuite() do the
   // call to require(), however that breaks the build system which looks for
   // the strings passed to require
   examiner.addSuite('gclitest/testCanon', require('gclitest/testCanon'));
   examiner.addSuite('gclitest/testCli', require('gclitest/testCli'));
   examiner.addSuite('gclitest/testCompletion', require('gclitest/testCompletion'));
   examiner.addSuite('gclitest/testExec', require('gclitest/testExec'));
+  examiner.addSuite('gclitest/testFocus', require('gclitest/testFocus'));
   examiner.addSuite('gclitest/testHelp', require('gclitest/testHelp'));
   examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory'));
   examiner.addSuite('gclitest/testInputter', require('gclitest/testInputter'));
   examiner.addSuite('gclitest/testIncomplete', require('gclitest/testIncomplete'));
   examiner.addSuite('gclitest/testIntro', require('gclitest/testIntro'));
   examiner.addSuite('gclitest/testJs', require('gclitest/testJs'));
   examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard'));
   examiner.addSuite('gclitest/testMenu', require('gclitest/testMenu'));
+  examiner.addSuite('gclitest/testNode', require('gclitest/testNode'));
   examiner.addSuite('gclitest/testPref', require('gclitest/testPref'));
   examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire'));
   examiner.addSuite('gclitest/testResource', require('gclitest/testResource'));
   examiner.addSuite('gclitest/testScratchpad', require('gclitest/testScratchpad'));
   examiner.addSuite('gclitest/testSettings', require('gclitest/testSettings'));
   examiner.addSuite('gclitest/testSpell', require('gclitest/testSpell'));
   examiner.addSuite('gclitest/testSplit', require('gclitest/testSplit'));
   examiner.addSuite('gclitest/testTokenize', require('gclitest/testTokenize'));
@@ -742,16 +776,24 @@ define('test/status', ['require', 'expor
  */
 
 define('gclitest/testCanon', ['require', 'exports', 'module' , 'gclitest/helpers', 'gcli/canon', 'test/assert'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
   var canon = require('gcli/canon');
   var test = require('test/assert');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testAddRemove = function(options) {
     var startCount = canon.getCommands().length;
     var events = 0;
 
     var canonChange = function(ev) {
       events++;
     };
     canon.onCanonChange.add(canonChange);
@@ -783,17 +825,19 @@ define('gclitest/testCanon', ['require',
       typed: 'testadd',
       outputMatch: /^2$/
     });
 
     canon.removeCommand('testadd');
 
     test.is(canon.getCommands().length, startCount, 'remove command success');
     test.is(events, 3, 'remove event');
-    helpers.status(options, {
+
+    helpers.setInput('testadd');
+    helpers.check({
       typed: 'testadd',
       status: 'ERROR'
     });
 
     canon.addCommand({
       name: 'testadd',
       exec: function() {
         return 3;
@@ -808,17 +852,19 @@ define('gclitest/testCanon', ['require',
     });
 
     canon.removeCommand({
       name: 'testadd'
     });
 
     test.is(canon.getCommands().length, startCount, 'reremove command success');
     test.is(events, 5, 'reremove event');
-    helpers.status(options, {
+
+    helpers.setInput('testadd');
+    helpers.check({
       typed: 'testadd',
       status: 'ERROR'
     });
 
     canon.removeCommand({ name: 'nonexistant' });
     test.is(canon.getCommands().length, startCount, 'nonexistant1 command success');
     test.is(events, 5, 'nonexistant1 event');
 
@@ -847,120 +893,179 @@ define('gclitest/testCanon', ['require',
  */
 
 define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert', 'gcli/util'], function(require, exports, module) {
 
 
 var test = require('test/assert');
 var util = require('gcli/util');
 
-
-var cachedOptions = undefined;
-
-exports.setup = function(opts) {
-  cachedOptions = opts;
-};
-
-exports.shutdown = function(opts) {
-  cachedOptions = undefined;
+var helpers = exports;
+
+helpers._display = undefined;
+
+helpers.setup = function(options) {
+  helpers._display = options.display;
+};
+
+helpers.shutdown = function(options) {
+  helpers._display = undefined;
 };
 
 /**
- * Check that we can parse command input.
- * Doesn't execute the command, just checks that we grok the input properly:
- *
- * helpers.status({
- *   // Test inputs
- *   typed: "ech",           // Required
- *   cursor: 3,              // Optional cursor position
- *
- *   // Thing to check
- *   status: "INCOMPLETE",   // 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: "VVVIIIEEE",    // What state should the error markup be in
- * });
+ * Various functions to return the actual state of the command line
  */
-exports.status = function(options, checks) {
-  var requisition = options.display.requisition;
-  var inputter = options.display.inputter;
-  var completer = options.display.completer;
-
-  if (checks.typed != null) {
-    inputter.setInput(checks.typed);
+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 {
-    test.ok(false, "Missing typed for " + JSON.stringify(checks));
-    return;
-  }
-
-  if (checks.cursor != null) {
-    inputter.setCursor(checks.cursor);
-  }
-
-  if (checks.status != null) {
-    test.is(requisition.getStatus().toString(),
-            checks.status,
-            "status for " + checks.typed);
+    output += '  tooltipState: \'' + helpers._actual.tooltipState() + '\'\n';
   }
-
-  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 != 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);
-  }
+  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.
  */
-exports.setInput = function(typed, cursor) {
-  cachedOptions.display.inputter.setInput(typed);
+helpers.setInput = function(typed, cursor) {
+  helpers._display.inputter.setInput(typed);
 
   if (cursor) {
-    cachedOptions.display.inputter.setCursor({ start: cursor, end: cursor });
+    helpers._display.inputter.setCursor({ start: cursor, end: cursor });
   }
+
+  helpers._display.focusManager.onInputChange();
+};
+
+/**
+ * Simulate focusing the input field
+ */
+helpers.focusInput = function() {
+  helpers._display.inputter.focus();
 };
 
 /**
  * Simulate pressing TAB in the input field
  */
-exports.pressTab = function() {
-  // requisition.complete({ start: 5, end: 5 }, 0);
-
+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: util.KeyEvent.DOM_VK_TAB,
+    keyCode: keyCode,
     preventDefault: function() { },
     timeStamp: new Date().getTime()
   };
-  cachedOptions.display.inputter.onKeyDown(fakeEvent);
-  cachedOptions.display.inputter.onKeyUp(fakeEvent);
+  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
@@ -975,114 +1080,101 @@ exports.pressTab = function() {
  *     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 != null) {
-    test.is(cachedOptions.display.inputter.element.value,
-            checks.input,
-            'input');
+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 (checks.cursor != null) {
-    test.is(cachedOptions.display.inputter.element.selectionStart,
-            checks.cursor,
-            'cursor');
+  if ('current' in checks) {
+    test.is(helpers._actual.current(), checks.current, 'current');
   }
 
-  if (checks.status != null) {
-    test.is(requisition.getStatus().toString(),
-            checks.status,
-            'status');
+  if ('status' in checks) {
+    test.is(helpers._actual.status(), checks.status, 'status');
   }
 
-  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 ('markup' in checks) {
+    test.is(helpers._actual.markup(), checks.markup, 'markup');
+  }
+
+  if ('hints' in checks) {
+    test.is(helpers._actual.hints(), checks.hints, 'hints');
   }
 
-  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 ('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 (check.value != null) {
+      if ('value' in check) {
         test.is(assignment.value,
                 check.value,
-                'arg[\'' + paramName + '\'].value');
+                'arg.' + paramName + '.value');
       }
 
-      if (check.name != null) {
+      if ('name' in check) {
         test.is(assignment.value.name,
                 check.name,
-                'arg[\'' + paramName + '\'].name');
+                'arg.' + paramName + '.name');
       }
 
-      if (check.type != null) {
+      if ('type' in check) {
         test.is(assignment.arg.type,
                 check.type,
-                'arg[\'' + paramName + '\'].type');
+                'arg.' + paramName + '.type');
       }
 
-      if (check.arg != null) {
+      if ('arg' in check) {
         test.is(assignment.arg.toString(),
                 check.arg,
-                'arg[\'' + paramName + '\'].arg');
+                'arg.' + paramName + '.arg');
       }
 
-      if (check.status != null) {
+      if ('status' in check) {
         test.is(assignment.getStatus().toString(),
                 check.status,
-                'arg[\'' + paramName + '\'].status');
+                'arg.' + paramName + '.status');
       }
 
-      if (check.message != null) {
+      if ('message' in check) {
         test.is(assignment.getMessage(),
                 check.message,
-                'arg[\'' + paramName + '\'].message');
+                'arg.' + paramName + '.message');
       }
     });
   }
 };
 
 /**
  * Execute a command:
  *
@@ -1091,17 +1183,17 @@ exports.check = function(checks) {
  *   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
  * });
  */
-exports.exec = function(options, tests) {
+helpers.exec = function(options, tests) {
   var requisition = options.display.requisition;
   var inputter = options.display.inputter;
 
   tests = tests || {};
 
   if (tests.typed) {
     inputter.setInput(tests.typed);
   }
@@ -1152,17 +1244,17 @@ exports.exec = function(options, tests) 
     return;
   }
 
   var div = options.window.document.createElement('div');
   output.toDom(div);
   var displayed = div.textContent.trim();
 
   if (tests.outputMatch) {
-    function doTest(match, against) {
+    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) {
@@ -1687,16 +1779,17 @@ exports.setup = function() {
   exports.option1.type = types.getType('string');
   exports.option2.type = types.getType('number');
 
   types.registerType(exports.optionType);
   types.registerType(exports.optionValue);
 
   canon.addCommand(exports.tsv);
   canon.addCommand(exports.tsr);
+  canon.addCommand(exports.tso);
   canon.addCommand(exports.tse);
   canon.addCommand(exports.tsj);
   canon.addCommand(exports.tsb);
   canon.addCommand(exports.tss);
   canon.addCommand(exports.tsu);
   canon.addCommand(exports.tsn);
   canon.addCommand(exports.tsnDif);
   canon.addCommand(exports.tsnExt);
@@ -1713,16 +1806,17 @@ exports.setup = function() {
   canon.addCommand(exports.tshidden);
   canon.addCommand(exports.tscook);
   canon.addCommand(exports.tslong);
 };
 
 exports.shutdown = function() {
   canon.removeCommand(exports.tsv);
   canon.removeCommand(exports.tsr);
+  canon.removeCommand(exports.tso);
   canon.removeCommand(exports.tse);
   canon.removeCommand(exports.tsj);
   canon.removeCommand(exports.tsb);
   canon.removeCommand(exports.tss);
   canon.removeCommand(exports.tsu);
   canon.removeCommand(exports.tsn);
   canon.removeCommand(exports.tsnDif);
   canon.removeCommand(exports.tsnExt);
@@ -1806,19 +1900,34 @@ exports.tsv = {
 };
 
 exports.tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsr')
 };
 
+exports.tso = {
+  name: 'tso',
+  params: [ { name: 'text', type: 'string', defaultValue: null } ],
+  exec: createExec('tso')
+};
+
 exports.tse = {
   name: 'tse',
-  params: [ { name: 'node', type: 'node' } ],
+  params: [
+    { name: 'node', type: 'node' },
+    {
+      group: 'options',
+      params: [
+        { name: 'nodes', type: { name: 'nodelist' } },
+        { name: 'nodes2', type: { name: 'nodelist', allowEmpty: true } }
+      ]
+    }
+  ],
   exec: createExec('tse')
 };
 
 exports.tsj = {
   name: 'tsj',
   params: [ { name: 'javascript', type: 'javascript' } ],
   exec: createExec('tsj')
 };
@@ -2454,16 +2563,37 @@ exports.testOutstanding = function(optio
   helpers.check({
     input:  'tsg --txt1 ddd ',
     hints:                 'aaa [options]',
     markup: 'VVVVVVVVVVVVVVV'
   });
   */
 };
 
+exports.testCompleteIntoOptional = function(options) {
+  // From bug 779816
+  helpers.setInput('tso ');
+  helpers.check({
+    typed:  'tso ',
+    hints:      '[text]',
+    markup: 'VVVV',
+    status: 'VALID'
+  });
+
+  helpers.setInput('tso');
+  helpers.pressTab();
+  helpers.check({
+    typed:  'tso ',
+    hints:      '[text]',
+    markup: 'VVVV',
+    status: 'VALID'
+  });
+};
+
+
 });
 /*
  * 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
  *
@@ -2484,16 +2614,17 @@ var canon = require('gcli/canon');
 var mockCommands = require('gclitest/mockCommands');
 var nodetype = require('gcli/types/node');
 
 var test = require('test/assert');
 
 var actualExec;
 var actualOutput;
 var hideExec = false;
+var skip = 'skip';
 
 exports.setup = function() {
   mockCommands.setup();
   mockCommands.onCommandExec.add(commandExeced);
   canon.commandOutputManager.onOutput.add(commandOutputed);
 };
 
 exports.shutdown = function() {
@@ -2531,16 +2662,20 @@ function exec(command, expectedArgs) {
   }
 
   test.is(Object.keys(expectedArgs).length, Object.keys(actualExec.args).length,
           'Arg count: ' + command);
   Object.keys(expectedArgs).forEach(function(arg) {
     var expectedArg = expectedArgs[arg];
     var actualArg = actualExec.args[arg];
 
+    if (expectedArg === skip) {
+      return;
+    }
+
     if (Array.isArray(expectedArg)) {
       if (!Array.isArray(actualArg)) {
         test.ok(false, 'actual is not an array. ' + command + '/' + arg);
         return;
       }
 
       test.is(expectedArg.length, actualArg.length,
               'Array length: ' + command + '/' + arg);
@@ -2586,17 +2721,17 @@ exports.testExec = function(options) {
   exec('tsu --num 10', { num: 10 });
 
   // Bug 704829 - Enable GCLI Javascript parameters
   // The answer to this should be 2
   exec('tsj { 1 + 1 }', { javascript: '1 + 1' });
 
   var origDoc = nodetype.getDocument();
   nodetype.setDocument(mockDoc);
-  exec('tse :root', { node: mockBody });
+  exec('tse :root', { node: mockBody, nodes: skip, nodes2: skip });
   nodetype.setDocument(origDoc);
 
   exec('tsn dif fred', { text: 'fred' });
   exec('tsn exten fred', { text: 'fred' });
   exec('tsn extend fred', { text: 'fred' });
 
   exec('tselarr 1', { num: '1', arr: [ ] });
   exec('tselarr 1 a', { num: '1', arr: [ 'a' ] });
@@ -2617,23 +2752,79 @@ var mockDoc = {
     if (css === ':root') {
       return {
         length: 1,
         item: function(i) {
           return mockBody;
         }
       };
     }
-    throw new Error('mockDoc.querySelectorAll(\'' + css + '\') error');
+    else {
+      return {
+        length: 0,
+        item: function() { return null; }
+      };
+    }
   }
 };
 
 
 });
 /*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gclitest/testFocus', ['require', 'exports', 'module' , 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
+var helpers = require('gclitest/helpers');
+var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+  mockCommands.setup();
+  helpers.setup(options);
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+  helpers.shutdown(options);
+};
+
+exports.testBasic = function(options) {
+  helpers.focusInput();
+  helpers.exec(options, 'help');
+
+  helpers.setInput('tsn deep');
+  helpers.check({
+    input:  'tsn deep',
+    hints:          '',
+    markup: 'IIIVIIII',
+    cursor: 8,
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'false:default'
+  });
+
+  helpers.pressReturn();
+  helpers.check({
+    input:  'tsn deep',
+    hints:          '',
+    markup: 'IIIVIIII',
+    cursor: 8,
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'true:isError'
+  });
+};
+
+
+});
+/*
  * 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
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -2643,32 +2834,61 @@ var mockDoc = {
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 define('gclitest/testHelp', ['require', 'exports', 'module' , 'gclitest/helpers'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testHelpStatus = function(options) {
-    helpers.status(options, {
+    helpers.setInput('help');
+    helpers.check({
       typed:  'help',
       hints:      ' [search]',
       markup: 'VVVV',
       status: 'VALID'
     });
 
-    helpers.status(options, {
+    helpers.setInput('help ');
+    helpers.check({
+      typed:  'help ',
+      hints:       '[search]',
+      markup: 'VVVVV',
+      status: 'VALID'
+    });
+
+    // From bug 779816
+    helpers.setInput('help');
+    helpers.pressTab();
+    helpers.check({
+      typed:  'help ',
+      hints:       '[search]',
+      markup: 'VVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('help foo');
+    helpers.check({
       typed:  'help foo',
       markup: 'VVVVVVVV',
       status: 'VALID',
       hints:  ''
     });
 
-    helpers.status(options, {
+    helpers.setInput('help foo bar');
+    helpers.check({
       typed:  'help foo bar',
       markup: 'VVVVVVVVVVVV',
       status: 'VALID',
       hints:  ''
     });
   };
 
   exports.testHelpExec = function(options) {
@@ -2955,17 +3175,17 @@ exports.testBasic = function(options) {
 };
 
 exports.testCompleted = function(options) {
   helpers.setInput('tsela');
   helpers.pressTab();
   helpers.check({
     args: {
       command: { name: 'tselarr', type: 'Argument' },
-      num: { type: 'Argument' },
+      num: { type: 'BlankArgument' },
       arr: { type: 'ArrayArgument' },
     }
   });
 
   helpers.setInput('tsn dif ');
   helpers.check({
     input:  'tsn dif ',
     hints:          '<text>',
@@ -2983,65 +3203,65 @@ exports.testCompleted = function(options
   helpers.check({
     input:  'tsn dif ',
     hints:          '<text>',
     markup: 'VVVVVVVV',
     cursor: 8,
     status: 'ERROR',
     args: {
       command: { name: 'tsn dif', type: 'Argument' },
-      text: { type: 'Argument', status: 'INCOMPLETE' }
+      text: { type: 'BlankArgument', 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,
     status: 'ERROR',
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, 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,
     status: 'ERROR',
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'INCOMPLETE' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, 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',
     status: 'ERROR',
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: 'fred', status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, 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 --',
@@ -3078,17 +3298,17 @@ exports.testCase = function(options) {
   helpers.check({
     input:  'tsg AA',
     hints:        ' [options] -> aaa',
     markup: 'VVVVII',
     status: 'ERROR',
     args: {
       solo: { value: undefined, text: 'AA', status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
       txt2: { value: undefined, status: 'VALID' },
       num: { value: undefined, status: 'VALID' }
     }
   });
 };
 
 exports.testIncomplete = function(options) {
   var requisition = options.display.requisition;
@@ -3132,82 +3352,82 @@ exports.testHidden = function(options) {
   helpers.check({
     input:  'tshidden',
     hints:          ' [options]',
     markup: 'VVVVVVVV',
     status: 'VALID',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: undefined, status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --vis');
   helpers.check({
     input:  'tshidden --vis',
     hints:                'ible [options]',
     markup: 'VVVVVVVVVIIIII',
     status: 'ERROR',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: undefined, status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --invisiblestrin');
   helpers.check({
     input:  'tshidden --invisiblestrin',
     hints:                           ' [options]',
     markup: 'VVVVVVVVVEEEEEEEEEEEEEEEE',
     status: 'ERROR',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: undefined, status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --invisiblestring');
   helpers.check({
     input:  'tshidden --invisiblestring',
     hints:                            ' <string> [options]',
     markup: 'VVVVVVVVVIIIIIIIIIIIIIIIII',
     status: 'ERROR',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: undefined, status: 'INCOMPLETE' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --invisiblestring x');
   helpers.check({
     input:  'tshidden --invisiblestring x',
     hints:                              ' [options]',
     markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
     status: 'VALID',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: 'x', status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --invisibleboolea');
   helpers.check({
     input:  'tshidden --invisibleboolea',
     hints:                            ' [options]',
     markup: 'VVVVVVVVVEEEEEEEEEEEEEEEEE',
     status: 'ERROR',
     args: {
       visible: { value: undefined, status: 'VALID' },
       invisiblestring: { value: undefined, status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tshidden --invisibleboolean');
   helpers.check({
     input:  'tshidden --invisibleboolean',
     hints:                             ' [options]',
     markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVV',
@@ -3223,17 +3443,17 @@ exports.testHidden = function(options) {
   helpers.check({
     input:  'tshidden --visible xxx',
     markup: 'VVVVVVVVVVVVVVVVVVVVVV',
     status: 'VALID',
     hints:  '',
     args: {
       visible: { value: 'xxx', status: 'VALID' },
       invisiblestring: { value: undefined, status: 'VALID' },
-      invisibleboolean: { value: undefined, status: 'VALID' }
+      invisibleboolean: { value: false, status: 'VALID' }
     }
   });
 };
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
@@ -3250,30 +3470,40 @@ exports.testHidden = function(options) {
  * limitations under the License.
  */
 
 define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers', 'test/assert'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
   var test = require('test/assert');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testIntroStatus = function(options) {
     if (options.isFirefox) {
       test.log('Skipping testIntroStatus in Firefox.');
       return;
     }
 
-    helpers.status(options, {
+    helpers.setInput('intro');
+    helpers.check({
       typed:  'intro',
       markup: 'VVVVV',
       status: 'VALID',
       hints: ''
     });
 
-    helpers.status(options, {
+    helpers.setInput('intro foo');
+    helpers.check({
       typed:  'intro foo',
       markup: 'VVVVVVEEE',
       status: 'ERROR',
       hints: ''
     });
   };
 
   exports.testIntroExec = function(options) {
@@ -3671,20 +3901,19 @@ exports.testIncrDecr = function() {
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gclitest/testMenu', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
-
-
-var test = require('test/assert');
+define('gclitest/testMenu', ['require', 'exports', 'module' , 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
 var helpers = require('gclitest/helpers');
 var mockCommands = require('gclitest/mockCommands');
 
 
 exports.setup = function(options) {
   mockCommands.setup();
   helpers.setup(options);
 };
@@ -3700,28 +3929,303 @@ exports.testOptions = function(options) 
     input:  'tslong',
     markup: 'VVVVVV',
     status: 'ERROR',
     hints: ' <msg> [options]',
     args: {
       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' },
+      bool: { value: false, status: 'VALID' },
+      bool2: { value: false, status: 'VALID' },
       sel2: { value: undefined, status: 'VALID' },
       num2: { value: undefined, status: 'VALID' }
     }
   });
 };
 
 
 });
 
 /*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gclitest/testNode', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
+var test = require('test/assert');
+var helpers = require('gclitest/helpers');
+var mockCommands = require('gclitest/mockCommands');
+
+
+exports.setup = function(options) {
+  mockCommands.setup();
+  helpers.setup(options);
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+  helpers.shutdown(options);
+};
+
+exports.testNode = function(options) {
+  var requisition = options.display.requisition;
+
+  helpers.setInput('tse ');
+  helpers.check({
+    input:  'tse ',
+    hints:      '<node> [options]',
+    markup: 'VVVV',
+    cursor: 4,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: { status: 'INCOMPLETE', message: '' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :');
+  helpers.check({
+    input:  'tse :',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        arg: ' :',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :root');
+  helpers.check({
+    input:  'tse :root',
+    hints:           ' [options]',
+    markup: 'VVVVVVVVV',
+    cursor: 9,
+    current: 'node',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :root ');
+  helpers.check({
+    input:  'tse :root ',
+    hints:            '[options]',
+    markup: 'VVVVVVVVVV',
+    cursor: 10,
+    current: 'node',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root ', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          'root id');
+
+  helpers.setInput('tse #gcli-nomatch');
+  helpers.check({
+    input:  'tse #gcli-nomatch',
+    hints:                   ' [options]',
+    markup: 'VVVVIIIIIIIIIIIII',
+    cursor: 17,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' #gcli-nomatch',
+        status: 'INCOMPLETE',
+        message: 'No matches'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse #');
+  helpers.check({
+    input:  'tse #',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' #',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse .');
+  helpers.check({
+    input:  'tse .',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' .',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse *');
+  helpers.check({
+    input:  'tse *',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' *',
+        status: 'ERROR',
+        // message: 'Too many matches (128)'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+};
+
+exports.testNodes = function(options) {
+  var requisition = options.display.requisition;
+
+  helpers.setInput('tse :root --nodes *');
+  helpers.check({
+    input:  'tse :root --nodes *',
+    hints:                       ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVV',
+    current: 'nodes',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { arg: ' --nodes *', status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          '#gcli-input id');
+
+  helpers.setInput('tse :root --nodes2 div');
+  helpers.check({
+    input:  'tse :root --nodes2 div',
+    hints:                       ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+    cursor: 22,
+    current: 'nodes2',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { arg: ' --nodes2 div', status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          'root id');
+
+  helpers.setInput('tse --nodes ffff');
+  helpers.check({
+    input:  'tse --nodes ffff',
+    hints:                  ' <node> [options]',
+    markup: 'VVVVIIIIIIIVIIII',
+    cursor: 16,
+    current: 'nodes',
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'true:isError',
+    args: {
+      command: { name: 'tse' },
+      node: { value: undefined, arg: '', status: 'INCOMPLETE', message: '' },
+      nodes: { value: undefined, arg: ' --nodes ffff', status: 'INCOMPLETE', message: 'No matches' },
+      nodes2: { arg: '', status: 'VALID', message: '' },
+    }
+  });
+  /*
+  test.is(requisition.getAssignment('nodes2').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  */
+
+  helpers.setInput('tse --nodes2 ffff');
+  helpers.check({
+    input:  'tse --nodes2 ffff',
+    hints:                   ' <node> [options]',
+    markup: 'VVVVVVVVVVVVVVVVV',
+    cursor: 17,
+    current: 'nodes2',
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'false:default',
+    args: {
+      command: { name: 'tse' },
+      node: { value: undefined, arg: '', status: 'INCOMPLETE', message: '' },
+      nodes: { arg: '', status: 'VALID', message: '' },
+      nodes2: { arg: ' --nodes2 ffff', status: 'VALID', message: '' },
+    }
+  });
+  /*
+  test.is(requisition.getAssignment('nodes').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  test.is(requisition.getAssignment('nodes2').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  */
+};
+
+
+});
+/*
  * 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
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -3737,134 +4241,152 @@ define('gclitest/testPref', ['require', 
 
 var pref = require('gcli/commands/pref');
 var helpers = require('gclitest/helpers');
 var mockSettings = require('gclitest/mockSettings');
 var test = require('test/assert');
 
 
 exports.setup = function(options) {
+  helpers.setup(options);
+
   if (!options.isFirefox) {
     mockSettings.setup();
   }
   else {
     test.log('Skipping testPref in Firefox.');
   }
 };
 
 exports.shutdown = function(options) {
+  helpers.shutdown(options);
+
   if (!options.isFirefox) {
     mockSettings.shutdown();
   }
 };
 
 exports.testPrefShowStatus = function(options) {
   if (options.isFirefox) {
     test.log('Skipping testPrefShowStatus in Firefox.');
     return;
   }
 
-  helpers.status(options, {
+  helpers.setInput('pref s');
+  helpers.check({
     typed:  'pref s',
     hints:        'et',
     markup: 'IIIIVI',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show');
+  helpers.check({
     typed:  'pref show',
     hints:           ' <setting>',
     markup: 'VVVVVVVVV',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show ');
+  helpers.check({
     typed:  'pref show ',
     hints:            'allowSet',
     markup: 'VVVVVVVVVV',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBo');
+  helpers.check({
     typed:  'pref show tempTBo',
     hints:                   'ol',
     markup: 'VVVVVVVVVVIIIIIII',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBool');
+  helpers.check({
     typed:  'pref show tempTBool',
     markup: 'VVVVVVVVVVVVVVVVVVV',
     status: 'VALID',
     hints:  ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBool 4');
+  helpers.check({
     typed:  'pref show tempTBool 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVE',
     status: 'ERROR',
     hints:  ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempNumber 4');
+  helpers.check({
     typed:  'pref show tempNumber 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVVE',
     status: 'ERROR',
     hints:  ''
   });
 };
 
 exports.testPrefSetStatus = function(options) {
   if (options.isFirefox) {
     test.log('Skipping testPrefSetStatus in Firefox.');
     return;
   }
 
-  helpers.status(options, {
+  helpers.setInput('pref s');
+  helpers.check({
     typed:  'pref s',
     hints:        'et',
     markup: 'IIIIVI',
     status: 'ERROR',
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set');
+  helpers.check({
     typed:  'pref set',
     hints:          ' <setting> <value>',
     markup: 'VVVVVVVV',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref xxx');
+  helpers.check({
     typed:  'pref xxx',
     markup: 'EEEEVEEE',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set ');
+  helpers.check({
     typed:  'pref set ',
     hints:           'allowSet <value>',
     markup: 'VVVVVVVVV',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempTBo');
+  helpers.check({
     typed:  'pref set tempTBo',
     hints:                  'ol <value>',
     markup: 'VVVVVVVVVIIIIIII',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempTBool 4');
+  helpers.check({
     typed:  'pref set tempTBool 4',
     markup: 'VVVVVVVVVVVVVVVVVVVE',
     status: 'ERROR',
     hints: ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempNumber 4');
+  helpers.check({
     typed:  'pref set tempNumber 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVV',
     status: 'VALID',
     hints: ''
   });
 };
 
 exports.testPrefExec = function(options) {
@@ -5062,16 +5584,20 @@ exports.testDefault = function(options) 
     // boolean and array types are exempt from needing undefined blank values
     if (type.name === 'boolean') {
       test.is(blank, false, 'blank boolean is false');
     }
     else if (type.name === 'array') {
       test.ok(Array.isArray(blank), 'blank array is array');
       test.is(blank.length, 0, 'blank array is empty');
     }
+    else if (type.name === 'nodelist') {
+      test.ok(typeof blank.item, 'function', 'blank.item is function');
+      test.is(blank.length, 0, 'blank nodelist is empty');
+    }
     else {
       test.is(blank, undefined, 'default defined for ' + type.name);
     }
   });
 };
 
 exports.testNullDefault = function(options) {
   forEachType({ defaultValue: null }, function(type) {
@@ -5815,24 +6341,26 @@ let testModuleNames = [
   'test/assert',
   'test/status',
   'gclitest/testCanon',
   'gclitest/helpers',
   'gclitest/testCli',
   'gclitest/mockCommands',
   'gclitest/testCompletion',
   'gclitest/testExec',
+  'gclitest/testFocus',
   'gclitest/testHelp',
   'gclitest/testHistory',
   'gclitest/testInputter',
   'gclitest/testIncomplete',
   'gclitest/testIntro',
   'gclitest/testJs',
   'gclitest/testKeyboard',
   'gclitest/testMenu',
+  'gclitest/testNode',
   'gclitest/testPref',
   'gclitest/mockSettings',
   'gclitest/testRequire',
   'gclitest/requirable',
   'gclitest/testResource',
   'gclitest/testScratchpad',
   'gclitest/testSettings',
   'gclitest/testSpell',