text: /*
   * Copyright 2004 ThoughtWorks, Inc
   *
   *  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
   *
   *  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.
   *
   */
  
  // This script contains a badly-organised collection of miscellaneous
  // functions that really better homes.
  
  function classCreate() {
      return function() {
        this.initialize.apply(this, arguments);
      }
  }
  
  function objectExtend(destination, source) {
    for (var property in source) {
      destination[property] = source[property];
    }
    return destination;
  }
  
  function sel$() {
    var results = [], element;
    for (var i = 0; i < arguments.length; i++) {
      element = arguments[i];
      if (typeof element == 'string')
        element = document.getElementById(element);
      results[results.length] = element;
    }
    return results.length < 2 ? results[0] : results;
  }
  
  function sel$A(iterable) {
    if (!iterable) return [];
    if (iterable.toArray) {
      return iterable.toArray();
    } else {
      var results = [];
      for (var i = 0; i < iterable.length; i++)
        results.push(iterable[i]);
      return results;
    }
  }
  
  function fnBind() {
    var args = sel$A(arguments), __method = args.shift(), object = args.shift();
    var retval = function() {
      return __method.apply(object, args.concat(sel$A(arguments)));
    }
    retval.__method = __method;
    return retval;
  }
  
  function fnBindAsEventListener(fn, object) {
    var __method = fn;
    return function(event) {
      return __method.call(object, event || window.event);
    }
  }
  
  function removeClassName(element, name) {
      var re = new RegExp("\\b" + name + "\\b", "g");
      element.className = element.className.replace(re, "");
  }
  
  function addClassName(element, name) {
      element.className = element.className + ' ' + name;
  }
  
  function elementSetStyle(element, style) {
      for (var name in style) {
        var value = style[name];
        if (value == null) value = "";
        element.style[name] = value;
      }
  }
  
  function elementGetStyle(element, style) {
      var value = element.style[style];
      if (!value) {
        if (document.defaultView && document.defaultView.getComputedStyle) {
          var css = document.defaultView.getComputedStyle(element, null);
          value = css ? css.getPropertyValue(style) : null;
        } else if (element.currentStyle) {
          value = element.currentStyle[style];
        }
      }
  
      /** DGF necessary? 
      if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
        if (Element.getStyle(element, 'position') == 'static') value = 'auto'; */
  
      return value == 'auto' ? null : value;
    }
  
  String.prototype.trim = function() {
      var result = this.replace(/^\s+/g, "");
      // strip leading
      return result.replace(/\s+$/g, "");
      // strip trailing
  };
  String.prototype.lcfirst = function() {
      return this.charAt(0).toLowerCase() + this.substr(1);
  };
  String.prototype.ucfirst = function() {
      return this.charAt(0).toUpperCase() + this.substr(1);
  };
  String.prototype.startsWith = function(str) {
      return this.indexOf(str) == 0;
  };
  
  // Returns the text in this element
  function getText(element) {
      var text = "";
  
      var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5");
      if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) {
          text = getTextContent(element);
      } else if (element.textContent) {
          text = element.textContent;
      } else if (element.innerText) {
          text = element.innerText;
      }
  
      text = normalizeNewlines(text);
      text = normalizeSpaces(text);
  
      return text.trim();
  }
  
  function getTextContent(element, preformatted) {
      if (element.nodeType == 3 /*Node.TEXT_NODE*/) {
          var text = element.data;
          if (!preformatted) {
              text = text.replace(/\n|\r|\t/g, " ");
          }
          return text;
      }
      if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) {
          var childrenPreformatted = preformatted || (element.tagName == "PRE");
          var text = "";
          for (var i = 0; i < element.childNodes.length; i++) {
              var child = element.childNodes.item(i);
              text += getTextContent(child, childrenPreformatted);
          }
          // Handle block elements that introduce newlines
          // -- From HTML spec:
          //<!ENTITY % block
          //     "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT |
          //      BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS">
          //
          // TODO: should potentially introduce multiple newlines to separate blocks
          if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") {
              text += "\n";
          }
          return text;
      }
      return '';
  }
  
  /**
   * Convert all newlines to \m
   */
  function normalizeNewlines(text)
  {
      return text.replace(/\r\n|\r/g, "\n");
  }
  
  /**
   * Replace multiple sequential spaces with a single space, and then convert &nbsp; to space.
   */
  function normalizeSpaces(text)
  {
      // IE has already done this conversion, so doing it again will remove multiple nbsp
      if (browserVersion.isIE)
      {
          return text;
      }
  
      // Replace multiple spaces with a single space
      // TODO - this shouldn't occur inside PRE elements
      text = text.replace(/\ +/g, " ");
  
      // Replace &nbsp; with a space
      var nbspPattern = new RegExp(String.fromCharCode(160), "g");
      if (browserVersion.isSafari) {
  	return replaceAll(text, String.fromCharCode(160), " ");
      } else {
  	return text.replace(nbspPattern, " ");
      }
  }
  
  function replaceAll(text, oldText, newText) {
      while (text.indexOf(oldText) != -1) {
  	text = text.replace(oldText, newText);
      }
      return text;
  }
  
  
  function xmlDecode(text) {
      text = text.replace(/&quot;/g, '"');
      text = text.replace(/&apos;/g, "'");
      text = text.replace(/&lt;/g, "<");
      text = text.replace(/&gt;/g, ">");
      text = text.replace(/&amp;/g, "&");
      return text;
  }
  
  // Sets the text in this element
  function setText(element, text) {
      if (element.textContent != null) {
          element.textContent = text;
      } else if (element.innerText != null) {
          element.innerText = text;
      }
  }
  
  // Get the value of an <input> element
  function getInputValue(inputElement) {
      if (inputElement.type) {
          if (inputElement.type.toUpperCase() == 'CHECKBOX' ||
              inputElement.type.toUpperCase() == 'RADIO')
          {
              return (inputElement.checked ? 'on' : 'off');
          }
      }
      if (inputElement.value == null) {
          throw new SeleniumError("This element has no value; is it really a form field?");
      }
      return inputElement.value;
  }
  
  /* Fire an event in a browser-compatible manner */
  function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
      canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
      if (element.fireEvent) {
          var evt = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);        
          element.fireEvent('on' + eventType, evt);
      }
      else {
          var evt = document.createEvent('HTMLEvents');
          
          try {
              evt.shiftKey = shiftKeyDown;
              evt.metaKey = metaKeyDown;
              evt.altKey = altKeyDown;
              evt.ctrlKey = controlKeyDown;
          } catch (e) {
              // On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent
              // we'll have to ignore them here
              LOG.exception(e);
          }
          
          evt.initEvent(eventType, canBubble, true);
          element.dispatchEvent(evt);
      }
  }
  
  function getKeyCodeFromKeySequence(keySequence) {
      var match = /^\\(\d{1,3})$/.exec(keySequence);
      if (match != null) {
          return match[1];
      }
      match = /^.$/.exec(keySequence);
      if (match != null) {
          return match[0].charCodeAt(0);
      }
      // this is for backward compatibility with existing tests
      // 1 digit ascii codes will break however because they are used for the digit chars
      match = /^\d{2,3}$/.exec(keySequence);
      if (match != null) {
          return match[0];
      }
      throw new SeleniumError("invalid keySequence");
  }
  
  function createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
       var evt = element.ownerDocument.createEventObject();
       evt.shiftKey = shiftKeyDown;
       evt.metaKey = metaKeyDown;
       evt.altKey = altKeyDown;
       evt.ctrlKey = controlKeyDown;
       return evt;
  }
  
  function triggerKeyEvent(element, eventType, keySequence, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
      var keycode = getKeyCodeFromKeySequence(keySequence);
      canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
      if (element.fireEvent) {
          var keyEvent = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
          keyEvent.keyCode = keycode;
          element.fireEvent('on' + eventType, keyEvent);
      }
      else {
          var evt;
          if (window.KeyEvent) {
              evt = document.createEvent('KeyEvents');
              evt.initKeyEvent(eventType, true, true, window, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown, keycode, keycode);
          } else {
              evt = document.createEvent('UIEvents');
              
              evt.shiftKey = shiftKeyDown;
              evt.metaKey = metaKeyDown;
              evt.altKey = altKeyDown;
              evt.ctrlKey = controlKeyDown;
  
              evt.initUIEvent(eventType, true, true, window, 1);
              evt.keyCode = keycode;
              evt.which = keycode;
          }
  
          element.dispatchEvent(evt);
      }
  }
  
  function removeLoadListener(element, command) {
      LOG.debug('Removing loadListenter for ' + element + ', ' + command);
      if (window.removeEventListener)
          element.removeEventListener("load", command, true);
      else if (window.detachEvent)
          element.detachEvent("onload", command);
  }
  
  function addLoadListener(element, command) {
      LOG.debug('Adding loadListenter for ' + element + ', ' + command);
      var augmentedCommand = function() {
          command.call(this, element);
      }
      if (window.addEventListener && !browserVersion.isOpera)
          element.addEventListener("load", augmentedCommand, true);
      else if (window.attachEvent)
          element.attachEvent("onload", augmentedCommand);
  }
  
  /**
   * Override the broken getFunctionName() method from JsUnit
   * This file must be loaded _after_ the jsunitCore.js
   */
  function getFunctionName(aFunction) {
      var regexpResult = aFunction.toString().match(/function (\w*)/);
      if (regexpResult && regexpResult[1]) {
          return regexpResult[1];
      }
      return 'anonymous';
  }
  
  function getDocumentBase(doc) {
      var bases = document.getElementsByTagName("base");
      if (bases && bases.length && bases[0].href) {
          return bases[0].href;
      }
      return "";
  }
  
  function getTagName(element) {
      var tagName;
      if (element && element.tagName && element.tagName.toLowerCase) {
          tagName = element.tagName.toLowerCase();
      }
      return tagName;
  }
  
  function selArrayToString(a) {
      if (isArray(a)) {
          // DGF copying the array, because the array-like object may be a non-modifiable nodelist
          var retval = [];
          for (var i = 0; i < a.length; i++) {
              var item = a[i];
              var replaced = new String(item).replace(/([,\\])/g, '\\$1');
              retval[i] = replaced;
          }
          return retval;
      }
      return new String(a);
  }
  
  
  function isArray(x) {
      return ((typeof x) == "object") && (x["length"] != null);
  }
  
  function absolutify(url, baseUrl) {
      /** returns a relative url in its absolute form, given by baseUrl.
      * 
      * This function is a little odd, because it can take baseUrls that
      * aren't necessarily directories.  It uses the same rules as the HTML 
      * &lt;base&gt; tag; if the baseUrl doesn't end with "/", we'll assume
      * that it points to a file, and strip the filename off to find its
      * base directory.
      *
      * So absolutify("foo", "http://x/bar") will return "http://x/foo" (stripping off bar),
      * whereas absolutify("foo", "http://x/bar/") will return "http://x/bar/foo" (preserving bar).
      * Naturally absolutify("foo", "http://x") will return "http://x/foo", appropriately.
      * 
      * @param url the url to make absolute; if this url is already absolute, we'll just return that, unchanged
      * @param baseUrl the baseUrl from which we'll absolutify, following the rules above.
      * @return 'url' if it was already absolute, or the absolutized version of url if it was not absolute.
      */
      
      // DGF isn't there some library we could use for this?
          
      if (/^\w+:/.test(url)) {
          // it's already absolute
          return url;
      }
      
      var loc;
      try {
          loc = parseUrl(baseUrl);
      } catch (e) {
          // is it an absolute windows file path? let's play the hero in that case
          if (/^\w:\\/.test(baseUrl)) {
              baseUrl = "file:///" + baseUrl.replace(/\\/g, "/");
              loc = parseUrl(baseUrl);
          } else {
              throw new SeleniumError("baseUrl wasn't absolute: " + baseUrl);
          }
      }
      loc.search = null;
      loc.hash = null;
      
      // if url begins with /, then that's the whole pathname
      if (/^\//.test(url)) {
          loc.pathname = url;
          var result = reassembleLocation(loc);
          return result;
      }
      
      // if pathname is null, then we'll just append "/" + the url
      if (!loc.pathname) {
          loc.pathname = "/" + url;
          var result = reassembleLocation(loc);
          return result;
      }
      
      // if pathname ends with /, just append url
      if (/\/$/.test(loc.pathname)) {
          loc.pathname += url;
          var result = reassembleLocation(loc);
          return result;
      }
      
      // if we're here, then the baseUrl has a pathname, but it doesn't end with /
      // in that case, we replace everything after the final / with the relative url
      loc.pathname = loc.pathname.replace(/[^\/\\]+$/, url);
      var result = reassembleLocation(loc);
      return result;
      
  }
  
  var URL_REGEX = /^((\w+):\/\/)(([^:]+):?([^@]+)?@)?([^\/\?:]*):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(.+)?/;
  
  function parseUrl(url) {
      var fields = ['url', null, 'protocol', null, 'username', 'password', 'host', 'port', 'pathname', 'search', 'hash'];
      var result = URL_REGEX.exec(url);
      if (!result) {
          throw new SeleniumError("Invalid URL: " + url);
      }
      var loc = new Object();
      for (var i = 0; i < fields.length; i++) {
          var field = fields[i];
          if (field == null) {
              continue;
          }
          loc[field] = result[i];
      }
      return loc;
  }
  
  function reassembleLocation(loc) {
      if (!loc.protocol) {
          throw new Error("Not a valid location object: " + o2s(loc));
      }
      var protocol = loc.protocol;
      protocol = protocol.replace(/:$/, "");
      var url = protocol + "://";
      if (loc.username) {
          url += loc.username;
          if (loc.password) {
              url += ":" + loc.password;
          }
          url += "@";
      }
      if (loc.host) {
          url += loc.host;
      }
      
      if (loc.port) {
          url += ":" + loc.port;
      }
      
      if (loc.pathname) {
          url += loc.pathname;
      }
      
      if (loc.search) {
          url += "?" + loc.search;
      }
      if (loc.hash) {
          var hash = loc.hash;
          hash = loc.hash.replace(/^#/, "");
          url += "#" + hash;
      }
      return url;
  }
  
  function canonicalize(url) {
      var tempLink = window.document.createElement("link");
      tempLink.href = url; // this will canonicalize the href on most browsers
      var loc = parseUrl(tempLink.href)
      if (!/\/\.\.\//.test(loc.pathname)) {
      	return tempLink.href;
      }
    	// didn't work... let's try it the hard way
    	var originalParts = loc.pathname.split("/");
    	var newParts = [];
    	newParts.push(originalParts.shift());
    	for (var i = 0; i < originalParts.length; i++) {
    		var part = originalParts[i];
    		if (".." == part) {
    			newParts.pop();
    			continue;
    		}
    		newParts.push(part);
    	}
    	loc.pathname = newParts.join("/");
      return reassembleLocation(loc);
  }
  
  function extractExceptionMessage(ex) {
      if (ex == null) return "null exception";
      if (ex.message != null) return ex.message;
      if (ex.toString && ex.toString() != null) return ex.toString();
  }
      
  
  function describe(object, delimiter) {
      var props = new Array();
      for (var prop in object) {
          try {
              props.push(prop + " -> " + object[prop]);
          } catch (e) {
              props.push(prop + " -> [htmlutils: ack! couldn't read this property! (Permission Denied?)]");
          }
      }
      return props.join(delimiter || '\n');
  }
  
  var PatternMatcher = function(pattern) {
      this.selectStrategy(pattern);
  };
  PatternMatcher.prototype = {
  
      selectStrategy: function(pattern) {
          this.pattern = pattern;
          var strategyName = 'glob';
          // by default
          if (/^([a-z-]+):(.*)/.test(pattern)) {
              var possibleNewStrategyName = RegExp.$1;
              var possibleNewPattern = RegExp.$2;
              if (PatternMatcher.strategies[possibleNewStrategyName]) {
                  strategyName = possibleNewStrategyName;
                  pattern = possibleNewPattern;
              }
          }
          var matchStrategy = PatternMatcher.strategies[strategyName];
          if (!matchStrategy) {
              throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName);
          }
          this.strategy = matchStrategy;
          this.matcher = new matchStrategy(pattern);
      },
  
      matches: function(actual) {
          return this.matcher.matches(actual + '');
          // Note: appending an empty string avoids a Konqueror bug
      }
  
  };
  
  /**
   * A "static" convenience method for easy matching
   */
  PatternMatcher.matches = function(pattern, actual) {
      return new PatternMatcher(pattern).matches(actual);
  };
  
  PatternMatcher.strategies = {
  
  /**
   * Exact matching, e.g. "exact:***"
   */
      exact: function(expected) {
          this.expected = expected;
          this.matches = function(actual) {
              return actual == this.expected;
          };
      },
  
  /**
   * Match by regular expression, e.g. "regexp:^[0-9]+$"
   */
      regexp: function(regexpString) {
          this.regexp = new RegExp(regexpString);
          this.matches = function(actual) {
              return this.regexp.test(actual);
          };
      },
  
      regex: function(regexpString) {
          this.regexp = new RegExp(regexpString);
          this.matches = function(actual) {
              return this.regexp.test(actual);
          };
      },
  
  /**
   * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*",
   * but don't require a perfect match; instead succeed if actual
   * contains something that matches globString.
   * Making this distinction is motivated by a bug in IE6 which
   * leads to the browser hanging if we implement *TextPresent tests
   * by just matching against a regular expression beginning and
   * ending with ".*".  The globcontains strategy allows us to satisfy
   * the functional needs of the *TextPresent ops more efficiently
   * and so avoid running into this IE6 freeze.
   */
      globContains: function(globString) {
          this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString));
          this.matches = function(actual) {
              return this.regexp.test(actual);
          };
      },
  
  
  /**
   * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*"
   */
      glob: function(globString) {
          this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString));
          this.matches = function(actual) {
              return this.regexp.test(actual);
          };
      }
  
  };
  
  PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) {
      var re = glob;
      re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1");
      re = re.replace(/\?/g, "(.|[\r\n])");
      re = re.replace(/\*/g, "(.|[\r\n])*");
      return re;
  };
  
  PatternMatcher.regexpFromGlobContains = function(globContains) {
      return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains);
  };
  
  PatternMatcher.regexpFromGlob = function(glob) {
      return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$";
  };
  
  var Assert = {
  
      fail: function(message) {
          throw new AssertionFailedError(message);
      },
  
  /*
  * Assert.equals(comment?, expected, actual)
  */
      equals: function() {
          var args = new AssertionArguments(arguments);
          if (args.expected === args.actual) {
              return;
          }
          Assert.fail(args.comment +
                      "Expected '" + args.expected +
                      "' but was '" + args.actual + "'");
      },
  
  /*
  * Assert.matches(comment?, pattern, actual)
  */
      matches: function() {
          var args = new AssertionArguments(arguments);
          if (PatternMatcher.matches(args.expected, args.actual)) {
              return;
          }
          Assert.fail(args.comment +
                      "Actual value '" + args.actual +
                      "' did not match '" + args.expected + "'");
      },
  
  /*
  * Assert.notMtches(comment?, pattern, actual)
  */
      notMatches: function() {
          var args = new AssertionArguments(arguments);
          if (!PatternMatcher.matches(args.expected, args.actual)) {
              return;
          }
          Assert.fail(args.comment +
                      "Actual value '" + args.actual +
                      "' did match '" + args.expected + "'");
      }
  
  };
  
  // Preprocess the arguments to allow for an optional comment.
  function AssertionArguments(args) {
      if (args.length == 2) {
          this.comment = "";
          this.expected = args[0];
          this.actual = args[1];
      } else {
          this.comment = args[0] + "; ";
          this.expected = args[1];
          this.actual = args[2];
      }
  }
  
  function AssertionFailedError(message) {
      this.isAssertionFailedError = true;
      this.isSeleniumError = true;
      this.message = message;
      this.failureMessage = message;
  }
  
  function SeleniumError(message) {
      var error = new Error(message);
      if (typeof(arguments.caller) != 'undefined') { // IE, not ECMA
          var result = '';
          for (var a = arguments.caller; a != null; a = a.caller) {
              result += '> ' + a.callee.toString() + '\n';
              if (a.caller == a) {
                  result += '*';
                  break;
              }
          }
          error.stack = result;
      }
      error.isSeleniumError = true;
      return error;
  }
  
  function highlight(element) {
      var highLightColor = "yellow";
      if (element.originalColor == undefined) { // avoid picking up highlight
          element.originalColor = elementGetStyle(element, "background-color");
      }
      elementSetStyle(element, {"backgroundColor" : highLightColor});
      window.setTimeout(function() {
          try {
              //if element is orphan, probably page of it has already gone, so ignore
              if (!element.parentNode) {
                  return;
              }
              elementSetStyle(element, {"backgroundColor" : element.originalColor});
          } catch (e) {} // DGF unhighlighting is very dangerous and low priority
      }, 200);
  }
  
  
  
  // for use from vs.2003 debugger
  function o2s(obj) {
      var s = "";
      for (key in obj) {
          var line = key + "->" + obj[key];
          line.replace("\n", " ");
          s += line + "\n";
      }
      return s;
  }
  
  var seenReadyStateWarning = false;
  
  function openSeparateApplicationWindow(url, suppressMozillaWarning) {
      // resize the Selenium window itself
      window.resizeTo(1200, 500);
      window.moveTo(window.screenX, 0);
  
      var appWindow = window.open(url + '?start=true', 'main');
      if (appWindow == null) {
          var errorMessage = "Couldn't open app window; is the pop-up blocker enabled?"
          LOG.error(errorMessage);
          throw new Error("Couldn't open app window; is the pop-up blocker enabled?");
      }
      try {
          var windowHeight = 500;
          if (window.outerHeight) {
              windowHeight = window.outerHeight;
          } else if (document.documentElement && document.documentElement.offsetHeight) {
              windowHeight = document.documentElement.offsetHeight;
          }
  
          if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft;
          if (window.screenTop && !window.screenY) window.screenY = window.screenTop;
  
          appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60);
          appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25);
      } catch (e) {
          LOG.error("Couldn't resize app window");
          LOG.exception(e);
      }
  
  
      if (!suppressMozillaWarning && window.document.readyState == null && !seenReadyStateWarning) {
          alert("Beware!  Mozilla bug 300992 means that we can't always reliably detect when a new page has loaded.  Install the Selenium IDE extension or the readyState extension available from selenium.openqa.org to make page load detection more reliable.");
          seenReadyStateWarning = true;
      }
  
      return appWindow;
  }
  
  var URLConfiguration = classCreate();
  objectExtend(URLConfiguration.prototype, {
      initialize: function() {
      },
      _isQueryParameterTrue: function (name) {
          var parameterValue = this._getQueryParameter(name);
          if (parameterValue == null) return false;
          if (parameterValue.toLowerCase() == "true") return true;
          if (parameterValue.toLowerCase() == "on") return true;
          return false;
      },
  
      _getQueryParameter: function(searchKey) {
          var str = this.queryString
          if (str == null) return null;
          var clauses = str.split('&');
          for (var i = 0; i < clauses.length; i++) {
              var keyValuePair = clauses[i].split('=', 2);
              var key = unescape(keyValuePair[0]);
              if (key == searchKey) {
                  return unescape(keyValuePair[1]);
              }
          }
          return null;
      },
  
      _extractArgs: function() {
          var str = SeleniumHTARunner.commandLine;
          if (str == null || str == "") return new Array();
          var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g);
          // We either want non quote stuff ([^"]+) surrounded by quotes
          // or we want to look-ahead, see that the next character isn't
          // a quoted argument, and then grab all the non-space stuff
          // this will return for the line: "foo" bar
          // the results "\"foo\"" and "bar"
  
          // So, let's unquote the quoted arguments:
          var args = new Array;
          for (var i = 0; i < matches.length; i++) {
              args[i] = matches[i];
              args[i] = args[i].replace(/^"(.*)"$/, "$1");
          }
          return args;
      },
  
      isMultiWindowMode:function() {
          return this._isQueryParameterTrue('multiWindow');
      },
      
      getBaseUrl:function() {
          return this._getQueryParameter('baseUrl');
              
      }
  });
  
  
  function safeScrollIntoView(element) {
      if (element.scrollIntoView) {
          element.scrollIntoView(false);
          return;
      }
      // TODO: work out how to scroll browsers that don't support
      // scrollIntoView (like Konqueror)
  }
  