docs: function UIElement(uiElementShorthand)
  {
      // a shorthand object might look like:
      //
      // {
      //     name: 'topic'
      //     , description: 'sidebar links to topic categories'
      //     , args: [
      //         {
      //             name: 'name'
      //             , description: 'the name of the topic'
      //             , defaultValues: topLevelTopics
      //         }
      //     ]
      //     , getLocator: function(args) {
      //         return this._listXPath +
      //             "/a[text()=" + args.name.quoteForXPath() + "]";
      //     }
      //     , getGenericLocator: function() {
      //         return this._listXPath + '/a';
      //     }
      //     // maintain testcases for getLocator()
      //     , testcase1: {
      //         // defaultValues used if args not specified
      //         args: { name: 'foo' }
      //         , xhtml: '<div id="topiclist">'
      //             + '<ul><li><a expected-result="1">foo</a></li></ul>'
      //             + '</div>'
      //     }
      //     // set a local element variable
      //     , _listXPath: "//div[@id='topiclist']/ul/li"
      // }
      //
      // name cannot be null or an empty string. Enforce the same requirement for
      // the description.
      
      /**
       * Recursively returns all permutations of argument-value pairs, given
       * a list of argument definitions. Each argument definition will have
       * a set of default values to use in generating said pairs. If an argument
       * has no default values defined, it will not be included among the
       * permutations.
       *
       * @param args        a list of UIArguments
       * @param inDocument  the document object to pass to the getDefaultValues()
       *                    method of each argument.
       *
       * @return  a list of associative arrays containing key value pairs
       */
      this.permuteArgs = function(args, inDocument) {
          if (args.length == 0) {
              return [];
          }
          
          var permutations = [];
          var arg = args[0];
          var remainingArgs = args.slice(1);
          var subsequentPermutations = this.permuteArgs(remainingArgs,
              inDocument);
          var defaultValues = arg.getDefaultValues(inDocument);
          
          // skip arguments for which no default values are defined. If the
          // argument is a required one, then no permutations are possible.
          if (defaultValues.length == 0) {
              if (arg.required) {
                  return [];
              }
              else {
                  return subsequentPermutations;
              }
          }
          
          for (var i = 0; i < defaultValues.length; ++i) {
              var value = defaultValues[i];
              var permutation;
              
              if (subsequentPermutations.length == 0) {
                  permutation = {};
                  permutation[arg.name] = value + "";
                  permutations.push(permutation);
              }
              else {
                  for (var j = 0; j < subsequentPermutations.length; ++j) {
                      permutation = clone(subsequentPermutations[j]);
                      permutation[arg.name] = value + "";
                      permutations.push(permutation);
                  }
              }
          }
          
          return permutations;
      }
      
      
      
      /**
       * Returns a list of all testcases for this UIElement.
       */
      this.getTestcases = function()
      {
          return this.testcases;
      }
      
      
      
      /**
       * Run all unit tests, stopping at the first failure, if any. Return true
       * if no failures encountered, false otherwise. See the following thread
       * regarding use of getElementById() on XML documents created by parsing
       * text via the DOMParser:
       *
       * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/
       */
      this.test = function()
      {
          var parser = new DOMParser();
          var testcases = this.getTestcases();
          testcaseLoop: for (var i = 0; i < testcases.length; ++i) {
              var testcase = testcases[i];
              var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="'
                  + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>';
              var doc = parser.parseFromString(xhtml, "text/xml");
              if (doc.firstChild.nodeName == 'parsererror') {
                  safe_alert('Error parsing XHTML in testcase "' + testcase.name
                      + '" for UI element "' + this.name + '": ' + "\n"
                      + doc.firstChild.firstChild.nodeValue);
              }
              
              // we're no longer using the default locators when testing, because
              // args is now required
              var locator = parse_locator(this.getLocator(testcase.args));
              var results;
              
              if (locator.type == 'xpath' || (locator.type == 'implicit' &&
                  locator.string.substring(0, 2) == '//')) {
                  // try using the javascript xpath engine to avoid namespace
                  // issues. The xpath does have to be lowercase however, it
                  // seems. 
                  results = eval_xpath(locator.string, doc,
                      { allowNativeXpath: false, returnOnFirstMatch: true });
              }
              else {
                  // piece the locator back together
                  locator = (locator.type == 'implicit')
                      ? locator.string
                      : locator.type + '=' + locator.string;
                  results = eval_locator(locator, doc);
              }
              
              if (results.length && results[0].hasAttribute('expected-result')) {
                  continue testcaseLoop;
              }
              
              // testcase failed
              if (is_IDE()) {
                  var msg = 'Testcase "' + testcase.name
                      + '" failed for UI element "' + this.name + '":';
                  if (!results.length) {
                      msg += '\n"' + (locator.string || locator) + '" did not match any elements!';
                  }
                  else {
                      msg += '\n' + results[0] + ' was not the expected result!';
                  }
                  safe_alert(msg);
              }
              return false;
          }
          return true;
      };
      
      
      
      /**
       * Creates a set of locators using permutations of default values for
       * arguments used in the locator construction. The set is returned as an
       * object mapping locators to key-value arguments objects containing the
       * values passed to getLocator() to create the locator.
       *
       * @param opt_inDocument (optional) the document object of the "current"
       *                       page when this method is invoked. Some arguments
       *                       may have default value lists that are calculated
       *                       based on the contents of the page.
       *
       * @return  a list of locator strings
       * @throws  UIElementException
       */
      this.getDefaultLocators = function(opt_inDocument) {
          var defaultLocators = {};
          if (this.args.length == 0) {
              defaultLocators[this.getLocator({})] = {};
          }
          else {
              var permutations = this.permuteArgs(this.args, opt_inDocument);
              if (permutations.length != 0) {
                  for (var i = 0; i < permutations.length; ++i) {
                      var args = permutations[i];
                      var locator = this.getLocator(args);
                      if (!locator) {
                          throw new UIElementException('Error in UIElement(): '
                              + 'no getLocator return value for element "' + name
                              + '"');
                      }
                      defaultLocators[locator] = args;
                  }
              }
              else {
                  // try using no arguments. Parse the locator to make sure it's
                  // really good. If it doesn't work, fine.
                  try {
                      var locator = this.getLocator();
                      parse_locator(locator);
                      defaultLocators[locator] = {};
                  }
                  catch (e) {
                      safe_log('debug', e.message);
                  }
              }
          }
          return defaultLocators;
      };
      
      
      
      /**
       * Validate the structure of the shorthand notation this object is being
       * initialized with. Throws an exception if there's a validation error.
       *
       * @param uiElementShorthand
       *
       * @throws  UIElementException
       */
      this.validate = function(uiElementShorthand)
      {
          var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
          if (!uiElementShorthand.name) {
              throw new UIElementException(msg + 'no name specified!');
          }
          if (!uiElementShorthand.description) {
              throw new UIElementException(msg + 'no description specified!');
          }
          if (!uiElementShorthand.locator
              && !uiElementShorthand.getLocator
              && !uiElementShorthand.xpath
              && !uiElementShorthand.getXPath) {
              throw new UIElementException(msg + 'no locator specified!');
          }
      };
      
      
      
      this.init = function(uiElementShorthand)
      {
          this.validate(uiElementShorthand);
          
          this.name = uiElementShorthand.name;
          this.description = uiElementShorthand.description;
          
          // construct a new getLocator() method based on the locator property,
          // or use the provided function. We're deprecating the xpath property
          // and getXPath() function, but still allow for them for backwards
          // compatability.
          if (uiElementShorthand.locator) {
              this.getLocator = function(args) {
                  return uiElementShorthand.locator;
              };
          }
          else if (uiElementShorthand.getLocator) {
              this.getLocator = uiElementShorthand.getLocator;
          }
          else if (uiElementShorthand.xpath) {
              this.getLocator = function(args) {
                  return uiElementShorthand.xpath;
              };
          }
          else {
              this.getLocator = uiElementShorthand.getXPath;
          }
          
          if (uiElementShorthand.genericLocator) {
              this.getGenericLocator = function() {
                  return uiElementShorthand.genericLocator;
              };
          }
          else if (uiElementShorthand.getGenericLocator) {
              this.getGenericLocator = uiElementShorthand.getGenericLocator;
          }
          
          if (uiElementShorthand.getOffsetLocator) {
              this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
          }
          
          // get the testcases and local variables
          this.testcases = [];
          var localVars = {};
          for (var attr in uiElementShorthand) {
              if (attr.match(/^testcase/)) {
                  var testcase = uiElementShorthand[attr];
                  if (uiElementShorthand.args &&
                      uiElementShorthand.args.length && !testcase.args) {
                      safe_alert('No args defined in ' + attr + ' for UI element '
                          + this.name + '! Skipping testcase.');
                      continue;
                  } 
                  testcase.name = attr;
                  this.testcases.push(testcase);
              }
              else if (attr.match(/^_/)) {
                  this[attr] = uiElementShorthand[attr];
                  localVars[attr] = uiElementShorthand[attr];
              }
          }
          
          // create the arguments
          this.args = []
          this.argsOrder = [];
          if (uiElementShorthand.args) {
              for (var i = 0; i < uiElementShorthand.args.length; ++i) {
                  var arg = new UIArgument(uiElementShorthand.args[i], localVars);
                  this.args.push(arg);
                  this.argsOrder.push(arg.name);
  
                  // if an exception is thrown when invoking getDefaultValues()
                  // with no parameters passed in, assume the method requires an
                  // inDocument parameter, and thus may only be invoked at run
                  // time. Mark the UI element object accordingly.
                  try {
                      arg.getDefaultValues();
                  }
                  catch (e) {
                      this.isDefaultLocatorConstructionDeferred = true;
                  }
              }
              
          }
          
          if (!this.isDefaultLocatorConstructionDeferred) {
              this.defaultLocators = this.getDefaultLocators();
          }
      };
      
      
      
      this.init(uiElementShorthand);
  }