Bug 424814: Update prototype test suite to 1.6.0.2, patch by harthur, r=sdwilsh
authorClint Talbert <ctalbert@mozilla.com>
Wed, 14 Jan 2009 11:19:07 -0600
changeset 23750 6eb627215b618ad6faf2f738305ada9f3430c6f0
parent 23749 a4ad1c1d61ddb1331de23016b14c75d5da67fa82
child 23751 aa198b35552db061b59e30cb6fff52c7e0ae2f43
push id4701
push userctalbert@mozilla.com
push dateThu, 15 Jan 2009 14:51:54 +0000
treeherdermozilla-central@6eb627215b61 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssdwilsh
bugs424814
milestone1.9.2a1pre
Bug 424814: Update prototype test suite to 1.6.0.2, patch by harthur, r=sdwilsh
dom/tests/mochitest/ajax/prototype/Makefile.in
dom/tests/mochitest/ajax/prototype/dist/Makefile.in
dom/tests/mochitest/ajax/prototype/dist/prototype.js
dom/tests/mochitest/ajax/prototype/dist/prototype_update_helper.js
dom/tests/mochitest/ajax/prototype/manifest.json
dom/tests/mochitest/ajax/prototype/test/Makefile.in
dom/tests/mochitest/ajax/prototype/test/browser.html
dom/tests/mochitest/ajax/prototype/test/console.html
dom/tests/mochitest/ajax/prototype/test/functional/Makefile.in
dom/tests/mochitest/ajax/prototype/test/functional/event.html
dom/tests/mochitest/ajax/prototype/test/lib/Makefile.in
dom/tests/mochitest/ajax/prototype/test/lib/unittest.js
dom/tests/mochitest/ajax/prototype/test/test.css
dom/tests/mochitest/ajax/prototype/test/unit/Makefile.in
dom/tests/mochitest/ajax/prototype/test/unit/ajax.html
dom/tests/mochitest/ajax/prototype/test/unit/ajax_test.js
dom/tests/mochitest/ajax/prototype/test/unit/array.html
dom/tests/mochitest/ajax/prototype/test/unit/array_test.js
dom/tests/mochitest/ajax/prototype/test/unit/base.html
dom/tests/mochitest/ajax/prototype/test/unit/base_test.js
dom/tests/mochitest/ajax/prototype/test/unit/dom.html
dom/tests/mochitest/ajax/prototype/test/unit/dom_test.js
dom/tests/mochitest/ajax/prototype/test/unit/element_mixins.html
dom/tests/mochitest/ajax/prototype/test/unit/element_mixins_test.js
dom/tests/mochitest/ajax/prototype/test/unit/enumerable.html
dom/tests/mochitest/ajax/prototype/test/unit/enumerable_test.js
dom/tests/mochitest/ajax/prototype/test/unit/event_test.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/Makefile.in
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/ajax.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/ajax.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/array.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/base.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/base.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/data.json
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/dom.css
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/dom.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/dom.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/element_mixins.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/element_mixins.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/empty.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/enumerable.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/enumerable.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/event.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/form.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/hash.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/position.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/selector.html
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/string.js
dom/tests/mochitest/ajax/prototype/test/unit/fixtures/unittest.html
dom/tests/mochitest/ajax/prototype/test/unit/form.html
dom/tests/mochitest/ajax/prototype/test/unit/form_test.js
dom/tests/mochitest/ajax/prototype/test/unit/hash.html
dom/tests/mochitest/ajax/prototype/test/unit/hash_test.js
dom/tests/mochitest/ajax/prototype/test/unit/number_test.js
dom/tests/mochitest/ajax/prototype/test/unit/position.html
dom/tests/mochitest/ajax/prototype/test/unit/position_test.js
dom/tests/mochitest/ajax/prototype/test/unit/range.html
dom/tests/mochitest/ajax/prototype/test/unit/range_test.js
dom/tests/mochitest/ajax/prototype/test/unit/selector.html
dom/tests/mochitest/ajax/prototype/test/unit/selector_test.js
dom/tests/mochitest/ajax/prototype/test/unit/string.html
dom/tests/mochitest/ajax/prototype/test/unit/string_test.js
dom/tests/mochitest/ajax/prototype/test/unit/tmp/Makefile.in
dom/tests/mochitest/ajax/prototype/test/unit/tmp/ajax_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/array_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/base_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/dom_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/element_mixins_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/enumerable_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/event_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/form_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/hash_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/number_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/position_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/range_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/selector_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/string_test.html
dom/tests/mochitest/ajax/prototype/test/unit/tmp/unit_test.html
dom/tests/mochitest/ajax/prototype/test/unit/unit_tests.html
dom/tests/mochitest/ajax/prototype/test/unit/unittest_test.js
--- a/dom/tests/mochitest/ajax/prototype/Makefile.in
+++ b/dom/tests/mochitest/ajax/prototype/Makefile.in
@@ -11,20 +11,21 @@
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is mozilla.org code.
 #
 # The Initial Developer of the Original Code is
 # Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2007
+# Portions created by the Initial Developer are Copyright (C) 2008
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
 # or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -51,9 +52,9 @@ DIRS	= \
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
         test_Prototype.html \
         manifest.json \
         $(NULL)
 
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
--- a/dom/tests/mochitest/ajax/prototype/dist/Makefile.in
+++ b/dom/tests/mochitest/ajax/prototype/dist/Makefile.in
@@ -11,20 +11,21 @@
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is mozilla.org code.
 #
 # The Initial Developer of the Original Code is
 # Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2007
+# Portions created by the Initial Developer are Copyright (C) 2008
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
 # or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -44,9 +45,9 @@ relativesrcdir	= dom/tests/mochitest/aja
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
 	prototype.js \
         $(NULL)
 
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
--- a/dom/tests/mochitest/ajax/prototype/dist/prototype.js
+++ b/dom/tests/mochitest/ajax/prototype/dist/prototype.js
@@ -1,85 +1,157 @@
-/*  Prototype JavaScript framework, version 1.5.1_rc2
+/*  Prototype JavaScript framework, version 1.6.0.2
  *  (c) 2005-2007 Sam Stephenson
  *
  *  Prototype is freely distributable under the terms of an MIT-style license.
  *  For details, see the Prototype web site: http://www.prototypejs.org/
  *
-/*--------------------------------------------------------------------------*/
+ *--------------------------------------------------------------------------*/
 
 var Prototype = {
-  Version: '1.5.1_rc2',
+  Version: '1.6.0.2',
 
   Browser: {
     IE:     !!(window.attachEvent && !window.opera),
     Opera:  !!window.opera,
     WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
-    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
-  },
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+  },
+
   BrowserFeatures: {
     XPath: !!document.evaluate,
+    SelectorsAPI: !!document.querySelector,
     ElementExtensions: !!window.HTMLElement,
     SpecificElementExtensions:
-      (document.createElement('div').__proto__ !==
-       document.createElement('form').__proto__)
-  },
-
-  ScriptFragment: '<script[^>]*>([\u0001-\uFFFF]*?)</script>',
-  emptyFunction: function() {},
+      document.createElement('div')['__proto__'] &&
+      document.createElement('div')['__proto__'] !==
+        document.createElement('form')['__proto__']
+  },
+
+  ScriptFragment: '<script[^>]*>([^\\x00]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
   K: function(x) { return x }
-}
-
+};
+
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
 var Class = {
   create: function() {
-    return function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
       this.initialize.apply(this, arguments);
     }
+
+    Object.extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      var subclass = function() { };
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      parent.subclasses.push(klass);
+    }
+
+    for (var i = 0; i < properties.length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = Prototype.emptyFunction;
+
+    klass.prototype.constructor = klass;
+
+    return klass;
   }
-}
-
-var Abstract = new Object();
+};
+
+Class.Methods = {
+  addMethods: function(source) {
+    var ancestor   = this.superclass && this.superclass.prototype;
+    var properties = Object.keys(source);
+
+    if (!Object.keys({ toString: true }).length)
+      properties.push("toString", "valueOf");
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && Object.isFunction(value) &&
+          value.argumentNames().first() == "$super") {
+        var method = value, value = (function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method);
+
+        value.valueOf = method.valueOf.bind(method);
+        value.toString = method.toString.bind(method);
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
+  }
+};
+
+var Abstract = { };
 
 Object.extend = function(destination, source) {
-  for (var property in source) {
+  for (var property in source)
     destination[property] = source[property];
-  }
   return destination;
-}
+};
 
 Object.extend(Object, {
   inspect: function(object) {
     try {
-      if (object === undefined) return 'undefined';
+      if (Object.isUndefined(object)) return 'undefined';
       if (object === null) return 'null';
-      return object.inspect ? object.inspect() : object.toString();
+      return object.inspect ? object.inspect() : String(object);
     } catch (e) {
       if (e instanceof RangeError) return '...';
       throw e;
     }
   },
 
   toJSON: function(object) {
     var type = typeof object;
-    switch(type) {
+    switch (type) {
       case 'undefined':
       case 'function':
       case 'unknown': return;
       case 'boolean': return object.toString();
     }
+
     if (object === null) return 'null';
     if (object.toJSON) return object.toJSON();
-    if (object.ownerDocument === document) return;
+    if (Object.isElement(object)) return;
+
     var results = [];
     for (var property in object) {
       var value = Object.toJSON(object[property]);
-      if (value !== undefined)
-        results.push(property.toJSON() + ':' + value);
+      if (!Object.isUndefined(value))
+        results.push(property.toJSON() + ': ' + value);
     }
-    return '{' + results.join(',') + '}';
+
+    return '{' + results.join(', ') + '}';
+  },
+
+  toQueryString: function(object) {
+    return $H(object).toQueryString();
+  },
+
+  toHTML: function(object) {
+    return object && object.toHTML ? object.toHTML() : String.interpret(object);
   },
 
   keys: function(object) {
     var keys = [];
     for (var property in object)
       keys.push(property);
     return keys;
   },
@@ -87,116 +159,174 @@ Object.extend(Object, {
   values: function(object) {
     var values = [];
     for (var property in object)
       values.push(object[property]);
     return values;
   },
 
   clone: function(object) {
-    return Object.extend({}, object);
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return !!(object && object.nodeType == 1);
+  },
+
+  isArray: function(object) {
+    return object != null && typeof object == "object" &&
+      'splice' in object && 'join' in object;
+  },
+
+  isHash: function(object) {
+    return object && object instanceof Hash;
+  },
+
+  isFunction: function(object) {
+    return typeof object == "function" && typeof object.call == "function";
+  },
+
+  isString: function(object) {
+    return typeof object == "string";
+  },
+
+  isNumber: function(object) {
+    return typeof object == "number";
+  },
+
+  isUndefined: function(object) {
+    return typeof object == "undefined";
   }
 });
 
-Function.prototype.bind = function() {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function() {
-    return __method.apply(object, args.concat($A(arguments)));
-  }
-}
-
-Function.prototype.bindAsEventListener = function(object) {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function(event) {
-    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
-  }
-}
-
-Object.extend(Number.prototype, {
-  toColorPart: function() {
-    return this.toPaddedString(2, 16);
-  },
-
-  succ: function() {
-    return this + 1;
-  },
-
-  times: function(iterator) {
-    $R(0, this, true).each(iterator);
-    return this;
-  },
-
-  toPaddedString: function(length, radix) {
-    var string = this.toString(radix || 10);
-    return '0'.times(length - string.length) + string;
-  },
-
-  toJSON: function() {
-    return isFinite(this) ? this.toString() : 'null';
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
+      .replace(/\s+/g, '').split(',');
+    return names.length == 1 && !names[0] ? [] : names;
+  },
+
+  bind: function() {
+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
+  },
+
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
+  },
+
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
+  },
+
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  defer: function() {
+    var args = [0.01].concat($A(arguments));
+    return this.delay.apply(this, args);
+  },
+
+  wrap: function(wrapper) {
+    var __method = this;
+    return function() {
+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+    }
+  },
+
+  methodize: function() {
+    if (this._methodized) return this._methodized;
+    var __method = this;
+    return this._methodized = function() {
+      return __method.apply(null, [this].concat($A(arguments)));
+    };
   }
 });
 
 Date.prototype.toJSON = function() {
-  return '"' + this.getFullYear() + '-' +
-    (this.getMonth() + 1).toPaddedString(2) + '-' +
-    this.getDate().toPaddedString(2) + 'T' +
-    this.getHours().toPaddedString(2) + ':' +
-    this.getMinutes().toPaddedString(2) + ':' +
-    this.getSeconds().toPaddedString(2) + '"';
+  return '"' + this.getUTCFullYear() + '-' +
+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+    this.getUTCDate().toPaddedString(2) + 'T' +
+    this.getUTCHours().toPaddedString(2) + ':' +
+    this.getUTCMinutes().toPaddedString(2) + ':' +
+    this.getUTCSeconds().toPaddedString(2) + 'Z"';
 };
 
 var Try = {
   these: function() {
     var returnValue;
 
     for (var i = 0, length = arguments.length; i < length; i++) {
       var lambda = arguments[i];
       try {
         returnValue = lambda();
         break;
-      } catch (e) {}
+      } catch (e) { }
     }
 
     return returnValue;
   }
-}
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
 
 /*--------------------------------------------------------------------------*/
 
-var PeriodicalExecuter = Class.create();
-PeriodicalExecuter.prototype = {
+var PeriodicalExecuter = Class.create({
   initialize: function(callback, frequency) {
     this.callback = callback;
     this.frequency = frequency;
     this.currentlyExecuting = false;
 
     this.registerCallback();
   },
 
   registerCallback: function() {
     this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
   },
 
+  execute: function() {
+    this.callback(this);
+  },
+
   stop: function() {
     if (!this.timer) return;
     clearInterval(this.timer);
     this.timer = null;
   },
 
   onTimerEvent: function() {
     if (!this.currentlyExecuting) {
       try {
         this.currentlyExecuting = true;
-        this.callback(this);
+        this.execute();
       } finally {
         this.currentlyExecuting = false;
       }
     }
   }
-}
+});
 Object.extend(String, {
   interpret: function(value) {
     return value == null ? '' : String(value);
   },
   specialChar: {
     '\b': '\\b',
     '\t': '\\t',
     '\n': '\\n',
@@ -220,86 +350,93 @@ Object.extend(String.prototype, {
         result += source, source = '';
       }
     }
     return result;
   },
 
   sub: function(pattern, replacement, count) {
     replacement = this.gsub.prepareReplacement(replacement);
-    count = count === undefined ? 1 : count;
+    count = Object.isUndefined(count) ? 1 : count;
 
     return this.gsub(pattern, function(match) {
       if (--count < 0) return match[0];
       return replacement(match);
     });
   },
 
   scan: function(pattern, iterator) {
     this.gsub(pattern, iterator);
-    return this;
+    return String(this);
   },
 
   truncate: function(length, truncation) {
     length = length || 30;
-    truncation = truncation === undefined ? '...' : truncation;
+    truncation = Object.isUndefined(truncation) ? '...' : truncation;
     return this.length > length ?
-      this.slice(0, length - truncation.length) + truncation : this;
+      this.slice(0, length - truncation.length) + truncation : String(this);
   },
 
   strip: function() {
     return this.replace(/^\s+/, '').replace(/\s+$/, '');
   },
 
   stripTags: function() {
     return this.replace(/<\/?[^>]+>/gi, '');
   },
 
   stripScripts: function() {
     return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
   },
 
-  extractScripts: function() {
-    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
-    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
-    return (this.match(matchAll) || []).map(function(scriptTag) {
-      return (scriptTag.match(matchOne) || ['', ''])[1];
-    });
-  },
+  extractScripts: (function() {
+  	var matchAll = new RegExp(Prototype.ScriptFragment, 'ig');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'i');
+    var matchComments = new RegExp('<!--\\s*' + Prototype.ScriptFragment + '\\s*-->', 'i');
+
+  	return function() {
+      if (this.indexOf('<script') == -1) return [];
+      return (this.replace(matchComments, '').match(matchAll) || []).map(function(scriptTag) {
+        return (scriptTag.match(matchOne) || ['', ''])[1];
+      });
+    }
+  })(),
 
   evalScripts: function() {
     return this.extractScripts().map(function(script) { return eval(script) });
   },
 
   escapeHTML: function() {
     var self = arguments.callee;
     self.text.data = this;
-    return self.div.innerHTML;
+    return self.container.innerHTML.replace(/"/g, '&quot;');
   },
 
   unescapeHTML: function() {
-    var div = document.createElement('div');
-    div.innerHTML = this.stripTags();
+    var div = new Element('div');
+    // Safari requires the text nested inside another element to render correctly
+    div.innerHTML = '<pre>' + this.stripTags() + '</pre>';
+    div = div.firstChild;
     return div.childNodes[0] ? (div.childNodes.length > 1 ?
-      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      $A(div.childNodes).inject('', function(memo, node) { return memo + node.nodeValue }) :
       div.childNodes[0].nodeValue) : '';
   },
 
   toQueryParams: function(separator) {
     var match = this.strip().match(/([^?#]*)(#.*)?$/);
-    if (!match) return {};
-
-    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+    if (!match) return { };
+
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
       if ((pair = pair.split('='))[0]) {
         var key = decodeURIComponent(pair.shift());
         var value = pair.length > 1 ? pair.join('=') : pair[0];
         if (value != undefined) value = decodeURIComponent(value);
 
         if (key in hash) {
-          if (hash[key].constructor != Array) hash[key] = [hash[key]];
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
           hash[key].push(value);
         }
         else hash[key] = value;
       }
       return hash;
     });
   },
 
@@ -308,19 +445,17 @@ Object.extend(String.prototype, {
   },
 
   succ: function() {
     return this.slice(0, this.length - 1) +
       String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
   },
 
   times: function(count) {
-    var result = '';
-    for (var i = 0; i < count; i++) result += this;
-    return result;
+    return count < 1 ? '' : new Array(count + 1).join(this);
   },
 
   camelize: function() {
     var parts = this.split('-'), len = parts.length;
     if (len == 1) return parts[0];
 
     var camelized = this.charAt(0) == '-'
       ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
@@ -352,22 +487,33 @@ Object.extend(String.prototype, {
     if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
     return "'" + escapedString.replace(/'/g, '\\\'') + "'";
   },
 
   toJSON: function() {
     return this.inspect(true);
   },
 
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this;
+    if (str.blank()) return false;
+    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
   evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
     try {
-      if (!sanitize || (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(this)))
-        return eval('(' + this + ')');
-    } catch (e) {}
-    throw new SyntaxError('Badly formated JSON string: ' + this.inspect());
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
   },
 
   include: function(pattern) {
     return this.indexOf(pattern) > -1;
   },
 
   startsWith: function(pattern) {
     return this.indexOf(pattern) === 0;
@@ -379,289 +525,344 @@ Object.extend(String.prototype, {
   },
 
   empty: function() {
     return this == '';
   },
 
   blank: function() {
     return /^\s*$/.test(this);
-  }
-});
-
-if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
-  escapeHTML: function() {
-    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
-  },
-  unescapeHTML: function() {
-    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  },
+
+  interpolate: function(object, pattern) {
+    return new Template(this, pattern).evaluate(object);
   }
 });
 
 String.prototype.gsub.prepareReplacement = function(replacement) {
-  if (typeof replacement == 'function') return replacement;
+  if (Object.isFunction(replacement)) return replacement;
   var template = new Template(replacement);
   return function(match) { return template.evaluate(match) };
-}
+};
 
 String.prototype.parseQuery = String.prototype.toQueryParams;
 
 Object.extend(String.prototype.escapeHTML, {
-  div:  document.createElement('div'),
+  container: document.createElement('pre'),
   text: document.createTextNode('')
 });
 
-with (String.prototype.escapeHTML) div.appendChild(text);
-
-var Template = Class.create();
-Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
-Template.prototype = {
+String.prototype.escapeHTML.container.appendChild(String.prototype.escapeHTML.text);
+
+if (Prototype.Browser.IE) {
+  // IE converts all newlines to carriage returns so we swap them back
+  String.prototype.unescapeHTML = String.prototype.unescapeHTML.wrap(function(proceed) {
+    return proceed().replace(/\r/g, '\n')
+  });
+}
+
+if ('>'.escapeHTML() !== '&gt;') {
+  // Safari 3.x has issues with escaping the ">" character
+  (function() {
+    var escapeHTML = String.prototype.escapeHTML;
+    Object.extend(
+      String.prototype.escapeHTML = escapeHTML.wrap(function(proceed) {
+        return proceed().replace(/>/g, "&gt;")
+      }), {
+      container: escapeHTML.container,
+      text: escapeHTML.text
+    })
+  })();
+}
+
+if ('&'.escapeHTML() !== '&amp;') {
+  // Safari 2.x has issues with escaping html inside a "pre" element so we use the deprecated "xmp" element instead
+  Object.extend(String.prototype.escapeHTML, {
+    container: document.createElement('xmp'),
+    text: document.createTextNode('')
+  });
+  String.prototype.escapeHTML.container.appendChild(String.prototype.escapeHTML.text);
+}
+
+var Template = Class.create({
   initialize: function(template, pattern) {
     this.template = template.toString();
-    this.pattern  = pattern || Template.Pattern;
+    this.pattern = pattern || Template.Pattern;
   },
 
   evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
     return this.template.gsub(this.pattern, function(match) {
-      var before = match[1];
+      if (object == null) return '';
+
+      var before = match[1] || '';
       if (before == '\\') return match[2];
-      return before + String.interpret(object[match[3]]);
+
+      var ctx = object, expr = match[3];
+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+      match = pattern.exec(expr);
+      if (match == null) return before;
+
+      while (match != null) {
+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+        ctx = ctx[comp];
+        if (null == ctx || '' == match[3]) break;
+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+        match = pattern.exec(expr);
+      }
+
+      return before + String.interpret(ctx);
     });
   }
-}
-
-var $break    = new Object();
-var $continue = new Object();
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
 
 var Enumerable = {
-  each: function(iterator) {
+  each: function(iterator, context) {
     var index = 0;
     try {
       this._each(function(value) {
-        iterator(value, index++);
+        iterator.call(context, value, index++);
       });
     } catch (e) {
       if (e != $break) throw e;
     }
     return this;
   },
 
-  eachSlice: function(number, iterator) {
+  eachSlice: function(number, iterator, context) {
     var index = -number, slices = [], array = this.toArray();
+    if (number < 1) return array;
     while ((index += number) < array.length)
       slices.push(array.slice(index, index+number));
-    return slices.map(iterator);
-  },
-
-  all: function(iterator) {
+    return slices.collect(iterator, context);
+  },
+
+  all: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result = true;
     this.each(function(value, index) {
-      result = result && !!(iterator || Prototype.K)(value, index);
+      result = result && !!iterator.call(context, value, index);
       if (!result) throw $break;
     });
     return result;
   },
 
-  any: function(iterator) {
+  any: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result = false;
     this.each(function(value, index) {
-      if (result = !!(iterator || Prototype.K)(value, index))
+      if (result = !!iterator.call(context, value, index))
         throw $break;
     });
     return result;
   },
 
-  collect: function(iterator) {
+  collect: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var results = [];
     this.each(function(value, index) {
-      results.push((iterator || Prototype.K)(value, index));
+      results.push(iterator.call(context, value, index));
     });
     return results;
   },
 
-  detect: function(iterator) {
+  detect: function(iterator, context) {
     var result;
     this.each(function(value, index) {
-      if (iterator(value, index)) {
+      if (iterator.call(context, value, index)) {
         result = value;
         throw $break;
       }
     });
     return result;
   },
 
-  findAll: function(iterator) {
+  findAll: function(iterator, context) {
     var results = [];
     this.each(function(value, index) {
-      if (iterator(value, index))
+      if (iterator.call(context, value, index))
         results.push(value);
     });
     return results;
   },
 
-  grep: function(pattern, iterator) {
+  grep: function(filter, iterator, context) {
+    iterator = iterator || Prototype.K;
     var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
     this.each(function(value, index) {
-      var stringValue = value.toString();
-      if (stringValue.match(pattern))
-        results.push((iterator || Prototype.K)(value, index));
-    })
+      if (filter.match(value))
+        results.push(iterator.call(context, value, index));
+    });
     return results;
   },
 
   include: function(object) {
+    if (Object.isFunction(this.indexOf))
+      if (this.indexOf(object) != -1) return true;
+
     var found = false;
     this.each(function(value) {
       if (value == object) {
         found = true;
         throw $break;
       }
     });
     return found;
   },
 
   inGroupsOf: function(number, fillWith) {
-    fillWith = fillWith === undefined ? null : fillWith;
+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
     return this.eachSlice(number, function(slice) {
-      while(slice.length < number) slice.push(fillWith);
+      while (slice.length < number) slice.push(fillWith);
       return slice;
     });
   },
 
-  inject: function(memo, iterator) {
+  inject: function(memo, iterator, context) {
     this.each(function(value, index) {
-      memo = iterator(memo, value, index);
+      memo = iterator.call(context, memo, value, index);
     });
     return memo;
   },
 
   invoke: function(method) {
     var args = $A(arguments).slice(1);
     return this.map(function(value) {
       return value[method].apply(value, args);
     });
   },
 
-  max: function(iterator) {
+  max: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value >= result)
+      value = iterator.call(context, value, index);
+      if (result == null || value >= result)
         result = value;
     });
     return result;
   },
 
-  min: function(iterator) {
+  min: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value < result)
+      value = iterator.call(context, value, index);
+      if (result == null || value < result)
         result = value;
     });
     return result;
   },
 
-  partition: function(iterator) {
+  partition: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var trues = [], falses = [];
     this.each(function(value, index) {
-      ((iterator || Prototype.K)(value, index) ?
+      (iterator.call(context, value, index) ?
         trues : falses).push(value);
     });
     return [trues, falses];
   },
 
   pluck: function(property) {
     var results = [];
-    this.each(function(value, index) {
+    this.each(function(value) {
       results.push(value[property]);
     });
     return results;
   },
 
-  reject: function(iterator) {
+  reject: function(iterator, context) {
     var results = [];
     this.each(function(value, index) {
-      if (!iterator(value, index))
+      if (!iterator.call(context, value, index))
         results.push(value);
     });
     return results;
   },
 
-  sortBy: function(iterator) {
+  sortBy: function(iterator, context) {
     return this.map(function(value, index) {
-      return {value: value, criteria: iterator(value, index)};
+      return {
+        value: value,
+        criteria: iterator.call(context, value, index)
+      };
     }).sort(function(left, right) {
       var a = left.criteria, b = right.criteria;
       return a < b ? -1 : a > b ? 1 : 0;
     }).pluck('value');
   },
 
   toArray: function() {
     return this.map();
   },
 
   zip: function() {
     var iterator = Prototype.K, args = $A(arguments);
-    if (typeof args.last() == 'function')
+    if (Object.isFunction(args.last()))
       iterator = args.pop();
 
     var collections = [this].concat(args).map($A);
     return this.map(function(value, index) {
       return iterator(collections.pluck(index));
     });
   },
 
   size: function() {
     return this.toArray().length;
   },
 
   inspect: function() {
     return '#<Enumerable:' + this.toArray().inspect() + '>';
   }
-}
+};
 
 Object.extend(Enumerable, {
   map:     Enumerable.collect,
   find:    Enumerable.detect,
   select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
   member:  Enumerable.include,
-  entries: Enumerable.toArray
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
 });
-var $A = Array.from = function(iterable) {
+function $A(iterable) {
   if (!iterable) return [];
-  if (iterable.toArray) {
-    return iterable.toArray();
-  } else {
-    var results = [];
-    for (var i = 0, length = iterable.length; i < length; i++)
-      results.push(iterable[i]);
-    return results;
-  }
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length || 0, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
 }
 
 if (Prototype.Browser.WebKit) {
-  $A = Array.from = function(iterable) {
+  $A = function(iterable) {
     if (!iterable) return [];
-    if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
-      iterable.toArray) {
-      return iterable.toArray();
-    } else {
-      var results = [];
-      for (var i = 0, length = iterable.length; i < length; i++)
-        results.push(iterable[i]);
-      return results;
-    }
-  }
+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+        iterable.toArray) return iterable.toArray();
+    var length = iterable.length || 0, results = new Array(length);
+    while (length--) results[length] = iterable[length];
+    return results;
+  };
 }
 
+Array.from = $A;
+
 Object.extend(Array.prototype, Enumerable);
 
-if (!Array.prototype._reverse)
-  Array.prototype._reverse = Array.prototype.reverse;
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
 
 Object.extend(Array.prototype, {
   _each: function(iterator) {
     for (var i = 0, length = this.length; i < length; i++)
       iterator(this[i]);
   },
 
   clear: function() {
@@ -680,222 +881,246 @@ Object.extend(Array.prototype, {
   compact: function() {
     return this.select(function(value) {
       return value != null;
     });
   },
 
   flatten: function() {
     return this.inject([], function(array, value) {
-      return array.concat(value && value.constructor == Array ?
+      return array.concat(Object.isArray(value) ?
         value.flatten() : [value]);
     });
   },
 
   without: function() {
     var values = $A(arguments);
     return this.select(function(value) {
       return !values.include(value);
     });
   },
 
-  indexOf: function(object) {
-    for (var i = 0, length = this.length; i < length; i++)
-      if (this[i] == object) return i;
-    return -1;
-  },
-
   reverse: function(inline) {
     return (inline !== false ? this : this.toArray())._reverse();
   },
 
   reduce: function() {
     return this.length > 1 ? this : this[0];
   },
 
   uniq: function(sorted) {
     return this.inject([], function(array, value, index) {
       if (0 == index || (sorted ? array.last() != value : !array.include(value)))
         array.push(value);
       return array;
     });
   },
 
+  intersect: function(array) {
+    var length = array.length, i;
+    return this.uniq().findAll(function(item) {
+      i = length;
+      while (i--) if (item === array[i]) return true;
+      return false;
+    });
+  },
+
   clone: function() {
     return [].concat(this);
   },
 
   size: function() {
     return this.length;
   },
 
   inspect: function() {
     return '[' + this.map(Object.inspect).join(', ') + ']';
   },
 
   toJSON: function() {
     var results = [];
     this.each(function(object) {
       var value = Object.toJSON(object);
-      if (value !== undefined) results.push(value);
+      if (!Object.isUndefined(value)) results.push(value);
     });
-    return '[' + results.join(',') + ']';
+    return '[' + results.join(', ') + ']';
   }
 });
 
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+  Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+  i || (i = 0);
+  var length = this.length;
+  if (i < 0) i = length + i;
+  for (; i < length; i++)
+    if (this[i] === item) return i;
+  return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+  var n = this.slice(0, i).reverse().indexOf(item);
+  return (n < 0) ? n : i - n - 1;
+};
+
 Array.prototype.toArray = Array.prototype.clone;
 
 function $w(string) {
+  if (!Object.isString(string)) return [];
   string = string.strip();
   return string ? string.split(/\s+/) : [];
 }
 
 if (Prototype.Browser.Opera){
   Array.prototype.concat = function() {
     var array = [];
     for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
     for (var i = 0, length = arguments.length; i < length; i++) {
-      if (arguments[i].constructor == Array) {
+      if (Object.isArray(arguments[i])) {
         for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
           array.push(arguments[i][j]);
       } else {
         array.push(arguments[i]);
       }
     }
     return array;
-  }
-}
-var Hash = function(object) {
-  if (object instanceof Hash) this.merge(object);
-  else Object.extend(this, object || {});
-};
-
-Object.extend(Hash, {
-  toQueryString: function(obj) {
-    var parts = [];
-    parts.add = arguments.callee.addPair;
-
-    this.prototype._each.call(obj, function(pair) {
-      if (!pair.key) return;
-      var value = pair.value;
-
-      if (value && typeof value == 'object') {
-        if (value.constructor == Array) value.each(function(value) {
-          parts.add(pair.key, value);
-        });
-        return;
-      }
-      parts.add(pair.key, value);
-    });
-
-    return parts.join('&');
-  },
-
-  toJSON: function(object) {
-    var results = [];
-    this.prototype._each.call(object, function(pair) {
-      var value = Object.toJSON(pair.value);
-      if (value !== undefined) results.push(pair.key.toJSON() + ':' + value);
-    });
-    return '{' + results.join(',') + '}';
-  }
-});
-
-Hash.toQueryString.addPair = function(key, value, prefix) {
-  key = encodeURIComponent(key);
-  if (value === undefined) this.push(key);
-  else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
+  };
 }
-
-Object.extend(Hash.prototype, Enumerable);
-Object.extend(Hash.prototype, {
-  _each: function(iterator) {
-    for (var key in this) {
-      var value = this[key];
-      if (value && value == Hash.prototype[key]) continue;
-
-      var pair = [key, value];
-      pair.key = key;
-      pair.value = value;
-      iterator(pair);
-    }
-  },
-
-  keys: function() {
-    return this.pluck('key');
-  },
-
-  values: function() {
-    return this.pluck('value');
-  },
-
-  merge: function(hash) {
-    return $H(hash).inject(this, function(mergedHash, pair) {
-      mergedHash[pair.key] = pair.value;
-      return mergedHash;
-    });
-  },
-
-  remove: function() {
-    var result;
-    for(var i = 0, length = arguments.length; i < length; i++) {
-      var value = this[arguments[i]];
-      if (value !== undefined){
-        if (result === undefined) result = value;
-        else {
-          if (result.constructor != Array) result = [result];
-          result.push(value)
-        }
-      }
-      delete this[arguments[i]];
-    }
-    return result;
-  },
-
-  toQueryString: function() {
-    return Hash.toQueryString(this);
-  },
-
-  inspect: function() {
-    return '#<Hash:{' + this.map(function(pair) {
-      return pair.map(Object.inspect).join(': ');
-    }).join(', ') + '}>';
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator, context) {
+    $R(0, this, true).each(iterator, context);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
   },
 
   toJSON: function() {
-    return Hash.toJSON(this);
+    return isFinite(this) ? this.toString() : 'null';
   }
 });
 
+$w('abs round ceil floor').each(function(method){
+  Number.prototype[method] = Math[method].methodize();
+});
 function $H(object) {
-  if (object instanceof Hash) return object;
   return new Hash(object);
 };
 
-// Safari iterates over shadowed properties
-if (function() {
-  var i = 0, Test = function(value) { this.key = value };
-  Test.prototype.key = 'foo';
-  for (var property in new Test('bar')) i++;
-  return i > 1;
-}()) Hash.prototype._each = function(iterator) {
-  var cache = [];
-  for (var key in this) {
-    var value = this[key];
-    if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
-    cache.push(key);
-    var pair = [key, value];
-    pair.key = key;
-    pair.value = value;
-    iterator(pair);
+var Hash = Class.create(Enumerable, (function() {
+
+  function toQueryPair(key, value) {
+    if (Object.isUndefined(value)) return key;
+    return key + '=' + encodeURIComponent(String.interpret(value));
   }
-};
-ObjectRange = Class.create();
-Object.extend(ObjectRange.prototype, Enumerable);
-Object.extend(ObjectRange.prototype, {
+
+  return {
+    initialize: function(object) {
+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+    },
+
+    _each: function(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    },
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      // simulating poorly supported hasOwnProperty
+      if (this._object[key] !== Object.prototype[key])
+        return this._object[key];
+    },
+
+    unset: function(key) {
+      var value = this._object[key];
+      delete this._object[key];
+      return value;
+    },
+
+    toObject: function() {
+      return Object.clone(this._object);
+    },
+
+    keys: function() {
+      return this.pluck('key');
+    },
+
+    values: function() {
+      return this.pluck('value');
+    },
+
+    index: function(value) {
+      var match = this.detect(function(pair) {
+        return pair.value === value;
+      });
+      return match && match.key;
+    },
+
+    merge: function(object) {
+      return this.clone().update(object);
+    },
+
+    update: function(object) {
+      return new Hash(object).inject(this, function(result, pair) {
+        result.set(pair.key, pair.value);
+        return result;
+      });
+    },
+
+    toQueryString: function() {
+      return this.inject([], function(results, pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return results.concat(values.map(toQueryPair.curry(key)));
+        } else results.push(toQueryPair(key, values));
+        return results;
+      }).join('&');
+    },
+
+    inspect: function() {
+      return '#<Hash:{' + this.map(function(pair) {
+        return pair.map(Object.inspect).join(': ');
+      }).join(', ') + '}>';
+    },
+
+    toJSON: function() {
+      return Object.toJSON(this.toObject());
+    },
+
+    clone: function() {
+      return new Hash(this);
+    }
+  }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
   initialize: function(start, end, exclusive) {
     this.start = start;
     this.end = end;
     this.exclusive = exclusive;
   },
 
   _each: function(iterator) {
     var value = this.start;
@@ -911,29 +1136,43 @@ Object.extend(ObjectRange.prototype, {
     if (this.exclusive)
       return value < this.end;
     return value <= this.end;
   }
 });
 
 var $R = function(start, end, exclusive) {
   return new ObjectRange(start, end, exclusive);
-}
+};
 
 var Ajax = {
   getTransport: function() {
-    return Try.these(
-      function() {return new XMLHttpRequest()},
-      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
-      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    var transport = false;
+    Ajax.getTransport = Try.these(
+      function() {
+        /* fallback on activex xmlhttp to avoid IE7 local file-system read error */
+        if (Prototype.Browser.IE && window.location.href.indexOf('file://') == 0) throw 'skip';
+        transport = new XMLHttpRequest();
+        return function(){ return new XMLHttpRequest()}
+      },
+      function() {
+        transport = new ActiveXObject('Msxml2.XMLHTTP');
+        return function(){ return new ActiveXObject('Msxml2.XMLHTTP')}
+      },
+      function() {
+        transport = new ActiveXObject('Microsoft.XMLHTTP');
+        return function(){ return new ActiveXObject('Microsoft.XMLHTTP')}
+      }
     ) || false;
+
+    return transport;
   },
 
   activeRequestCount: 0
-}
+};
 
 Ajax.Responders = {
   responders: [],
 
   _each: function(iterator) {
     this.responders._each(iterator);
   },
 
@@ -943,112 +1182,128 @@ Ajax.Responders = {
   },
 
   unregister: function(responder) {
     this.responders = this.responders.without(responder);
   },
 
   dispatch: function(callback, request, transport, json) {
     this.each(function(responder) {
-      if (typeof responder[callback] == 'function') {
+      if (Object.isFunction(responder[callback])) {
         try {
           responder[callback].apply(responder, [request, transport, json]);
-        } catch (e) {}
+        } catch (e) { }
       }
     });
   }
 };
 
 Object.extend(Ajax.Responders, Enumerable);
 
 Ajax.Responders.register({
-  onCreate: function() {
-    Ajax.activeRequestCount++;
-  },
-  onComplete: function() {
-    Ajax.activeRequestCount--;
-  }
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
 });
 
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
-  setOptions: function(options) {
+Ajax.Base = Class.create({
+  initialize: function(options) {
+    this.allowStatusZero = false;
     this.options = {
       method:       'post',
       asynchronous: true,
       contentType:  'application/x-www-form-urlencoded',
       encoding:     'UTF-8',
-      parameters:   ''
-    }
-    Object.extend(this.options, options || {});
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
 
     this.options.method = this.options.method.toLowerCase();
-    if (typeof this.options.parameters == 'string')
+
+    if (Object.isString(this.options.parameters))
       this.options.parameters = this.options.parameters.toQueryParams();
+    else if (Object.isHash(this.options.parameters))
+      this.options.parameters = this.options.parameters.toObject();
   }
-}
-
-Ajax.Request = Class.create();
-Ajax.Request.Events =
-  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
-
-Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
   _complete: false,
-
-  initialize: function(url, options) {
+  _allowStatusZero: false,
+
+  initialize: function($super, url, options) {
+    $super(options);
     this.transport = Ajax.getTransport();
-    this.setOptions(options);
     this.request(url);
   },
 
-  request: function(url) {
-    this.url = url;
-    this.method = this.options.method;
-    var params = Object.clone(this.options.parameters);
-
-    if (!['get', 'post'].include(this.method)) {
-      // simulate other verbs over post
-      params['_method'] = this.method;
-      this.method = 'post';
+  request: (function() {
+
+    var absoluteExp = /^[a-z]{3,5}:/, fileExp = /^(file|ftp):/;
+    function isRelative(url) {
+      return !absoluteExp.test(url);
+    }
+
+    function isFileProtocol(url) {
+      return fileExp.test(url);
     }
 
-    this.parameters = params;
-
-    if (params = Hash.toQueryString(params)) {
-      // when GET, append parameters to URL
-      if (this.method == 'get')
+    return function(url) {
+      var base;
+      if (Prototype.Browser.Opera && opera.version() < 9.5 &&
+          isRelative(url) && (base = $(document.documentElement).down('base')))
+        url = base.readAttribute('href') + url;
+
+      this.url = url;
+      this.method = this.options.method;
+      var params = Object.clone(this.options.parameters);
+      this._allowStatusZero = isFileProtocol(this.url) ||
+        (isRelative(url) && isFileProtocol(window.location.protocol));
+
+      if (!['get', 'post'].include(this.method)) {
+        // simulate other verbs over post
+        params['_method'] = this.method;
+        this.method = 'post';
+      }
+
+      this.parameters = params;
+
+      if (params = Object.toQueryString(params)) {
         this.url += (this.url.include('?') ? '&' : '?') + params;
-      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
-        params += '&_=';
+        if (this.method == 'post' &&
+            /Konqueror|Safari|KHTML/.test(navigator.userAgent))
+          params += '&_=';
+      }
+
+      try {
+        var response = new Ajax.Response(this);
+        if (this.options.onCreate) this.options.onCreate(response);
+        Ajax.Responders.dispatch('onCreate', this, response);
+
+        this.transport.open(this.method.toUpperCase(), this.url,
+          this.options.asynchronous);
+
+        if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+        this.transport.onreadystatechange = this.onStateChange.bind(this);
+        this.setRequestHeaders();
+
+        this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+        this.transport.send(this.body);
+
+        /* Force Firefox to handle ready state 4 for synchronous requests */
+        if (!this.options.asynchronous && this.transport.overrideMimeType)
+          this.onStateChange();
+      }
+      catch (e) {
+        this.dispatchException(e);
+      }
     }
-
-    try {
-      Ajax.Responders.dispatch('onCreate', this, this.transport);
-
-      this.transport.open(this.method.toUpperCase(), this.url,
-        this.options.asynchronous);
-
-      if (this.options.asynchronous)
-        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
-
-      this.transport.onreadystatechange = this.onStateChange.bind(this);
-      this.setRequestHeaders();
-
-      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
-      this.transport.send(this.body);
-
-      /* Force Firefox to handle ready state 4 for synchronous requests */
-      if (!this.options.asynchronous && this.transport.overrideMimeType)
-        this.onStateChange();
-
-    }
-    catch (e) {
-      this.dispatchException(e);
-    }
-  },
+  })(),
 
   onStateChange: function() {
     var readyState = this.transport.readyState;
     if (readyState > 1 && !((readyState == 4) && this._complete))
       this.respondToReadyState(this.transport.readyState);
   },
 
   setRequestHeaders: function() {
@@ -1070,142 +1325,222 @@ Ajax.Request.prototype = Object.extend(n
           (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
             headers['Connection'] = 'close';
     }
 
     // user-defined headers
     if (typeof this.options.requestHeaders == 'object') {
       var extras = this.options.requestHeaders;
 
-      if (typeof extras.push == 'function')
+      if (Object.isFunction(extras.push))
         for (var i = 0, length = extras.length; i < length; i += 2)
           headers[extras[i]] = extras[i+1];
       else
         $H(extras).each(function(pair) { headers[pair.key] = pair.value });
     }
 
     for (var name in headers)
       this.transport.setRequestHeader(name, headers[name]);
   },
 
   success: function() {
-    return !this.transport.status
-        || (this.transport.status >= 200 && this.transport.status < 300);
+    var status = this.getStatus();
+    return (!status && this._allowStatusZero) || (status >= 200 && status < 300);
+  },
+
+  getStatus: function() {
+    try {
+      return this.transport.status || 0;
+    } catch (e) { return 0 }
   },
 
   respondToReadyState: function(readyState) {
-    var state = Ajax.Request.Events[readyState];
-    var transport = this.transport, json = this.evalJSON();
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
 
     if (state == 'Complete') {
       try {
         this._complete = true;
-        (this.options['on' + this.transport.status]
+        (this.options['on' + response.status]
          || this.options['on' + (this.success() ? 'Success' : 'Failure')]
-         || Prototype.emptyFunction)(transport, json);
+         || Prototype.emptyFunction)(response, response.headerJSON);
       } catch (e) {
         this.dispatchException(e);
       }
 
-      if ((this.getHeader('Content-type') || 'text/javascript').strip().
-        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
-          this.evalResponse();
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && this.isSameOrigin() && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+        this.evalResponse();
     }
 
     try {
-      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
-      Ajax.Responders.dispatch('on' + state, this, transport, json);
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
     } catch (e) {
       this.dispatchException(e);
     }
 
     if (state == 'Complete') {
       // avoid memory leak in MSIE: clean up
       this.transport.onreadystatechange = Prototype.emptyFunction;
     }
   },
 
+  isSameOrigin: function() {
+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+      protocol: location.protocol,
+      domain: document.domain,
+      port: location.port ? ':' + location.port : ''
+    }));
+  },
+
   getHeader: function(name) {
     try {
-      return this.transport.getResponseHeader(name);
-    } catch (e) { return null }
-  },
-
-  evalJSON: function() {
-    try {
-      var json = this.getHeader('X-JSON');
-      return json ? eval('(' + json + ')') : null;
+      return this.transport.getResponseHeader(name) || null;
     } catch (e) { return null }
   },
 
   evalResponse: function() {
     try {
-      return eval(this.transport.responseText);
+      return eval((this.transport.responseText || '').unfilterJSON());
     } catch (e) {
       this.dispatchException(e);
     }
   },
 
   dispatchException: function(exception) {
     (this.options.onException || Prototype.emptyFunction)(this, exception);
     Ajax.Responders.dispatch('onException', this, exception);
   }
 });
 
-Ajax.Updater = Class.create();
-
-Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
-  initialize: function(container, url, options) {
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+  initialize: function(request){
+    this.request = request;
+    var transport  = this.transport  = request.transport,
+        readyState = this.readyState = transport.readyState;
+
+    if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+      this.status       = this.getStatus();
+      this.statusText   = this.getStatusText();
+      this.responseText = String.interpret(transport.responseText);
+      this.headerJSON   = this._getHeaderJSON();
+    }
+
+    if (readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = Object.isUndefined(xml) ? null : xml;
+      this.responseJSON = this._getResponseJSON();
+    }
+  },
+
+  status:      0,
+  statusText: '',
+
+  getStatus: Ajax.Request.prototype.getStatus,
+
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
+  },
+
+  getHeader: Ajax.Request.prototype.getHeader,
+
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
+
+  getResponseHeader: function(name) {
+    return this.transport.getResponseHeader(name);
+  },
+
+  getAllResponseHeaders: function() {
+    return this.transport.getAllResponseHeaders();
+  },
+
+  _getHeaderJSON: function() {
+    var json = this.getHeader('X-JSON');
+    if (!json) return null;
+    json = decodeURIComponent(escape(json));
+    try {
+      return json.evalJSON(this.request.options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  },
+
+  _getResponseJSON: function() {
+    var options = this.request.options;
+    if (!options.evalJSON || (options.evalJSON != 'force' &&
+      !(this.getHeader('Content-type') || '').include('application/json')) ||
+        this.responseText.blank())
+          return null;
+    try {
+      return this.responseText.evalJSON(options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+  initialize: function($super, container, url, options) {
     this.container = {
       success: (container.success || container),
       failure: (container.failure || (container.success ? null : container))
-    }
-
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
-
-    var onComplete = this.options.onComplete || Prototype.emptyFunction;
-    this.options.onComplete = (function(transport, param) {
-      this.updateContent();
-      onComplete(transport, param);
+    };
+
+    options = Object.clone(options);
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, json) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, json);
     }).bind(this);
 
-    this.request(url);
-  },
-
-  updateContent: function() {
-    var receiver = this.container[this.success() ? 'success' : 'failure'];
-    var response = this.transport.responseText;
-
-    if (!this.options.evalScripts) response = response.stripScripts();
+    $super(url, options);
+  },
+
+  updateContent: function(responseText) {
+    var receiver = this.container[this.success() ? 'success' : 'failure'],
+        options = this.options;
+
+    if (!options.evalScripts) responseText = responseText.stripScripts();
 
     if (receiver = $(receiver)) {
-      if (this.options.insertion)
-        new this.options.insertion(receiver, response);
-      else
-        receiver.update(response);
-    }
-
-    if (this.success()) {
-      if (this.onComplete)
-        setTimeout(this.onComplete.bind(this), 10);
+      if (options.insertion) {
+        if (Object.isString(options.insertion)) {
+          var insertion = { }; insertion[options.insertion] = responseText;
+          receiver.insert(insertion);
+        }
+        else options.insertion(receiver, responseText);
+      }
+      else receiver.update(responseText);
     }
   }
 });
 
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
-  initialize: function(container, url, options) {
-    this.setOptions(options);
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+  initialize: function($super, container, url, options) {
+    $super(options);
     this.onComplete = this.options.onComplete;
 
     this.frequency = (this.options.frequency || 2);
     this.decay = (this.options.decay || 1);
 
-    this.updater = {};
+    this.updater = { };
     this.container = container;
     this.url = url;
 
     this.start();
   },
 
   start: function() {
     this.options.onComplete = this.updateComplete.bind(this);
@@ -1213,157 +1548,203 @@ Ajax.PeriodicalUpdater.prototype = Objec
   },
 
   stop: function() {
     this.updater.options.onComplete = undefined;
     clearTimeout(this.timer);
     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
   },
 
-  updateComplete: function(request) {
+  updateComplete: function(response) {
     if (this.options.decay) {
-      this.decay = (request.responseText == this.lastText ?
+      this.decay = (response.responseText == this.lastText ?
         this.decay * this.options.decay : 1);
 
-      this.lastText = request.responseText;
+      this.lastText = response.responseText;
     }
-    this.timer = setTimeout(this.onTimerEvent.bind(this),
-      this.decay * this.frequency * 1000);
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
   },
 
   onTimerEvent: function() {
     this.updater = new Ajax.Updater(this.container, this.url, this.options);
   }
 });
 function $(element) {
   if (arguments.length > 1) {
     for (var i = 0, elements = [], length = arguments.length; i < length; i++)
       elements.push($(arguments[i]));
     return elements;
   }
-  if (typeof element == 'string')
+  if (Object.isString(element))
     element = document.getElementById(element);
   return Element.extend(element);
 }
 
 if (Prototype.BrowserFeatures.XPath) {
   document._getElementsByXPath = function(expression, parentElement) {
     var results = [];
     var query = document.evaluate(expression, $(parentElement) || document,
       null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
     for (var i = 0, length = query.snapshotLength; i < length; i++)
-      results.push(query.snapshotItem(i));
+      results.push(Element.extend(query.snapshotItem(i)));
     return results;
   };
-
-  document.getElementsByClassName = function(className, parentElement) {
-    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
-    return document._getElementsByXPath(q, parentElement);
-  }
-
-} else document.getElementsByClassName = function(className, parentElement) {
-  var children = ($(parentElement) || document.body).getElementsByTagName('*');
-  var elements = [], child;
-  for (var i = 0, length = children.length; i < length; i++) {
-    child = children[i];
-    if (Element.hasClassName(child, className))
-      elements.push(Element.extend(child));
-  }
-  return elements;
-};
+}
 
 /*--------------------------------------------------------------------------*/
 
-if (!window.Element) var Element = {};
-
-Element.extend = function(element) {
-  var F = Prototype.BrowserFeatures;
-  if (!element || !element.tagName || element.nodeType == 3 ||
-   element._extended || F.SpecificElementExtensions || element == window)
-    return element;
-
-  var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
-   T = Element.Methods.ByTag;
-
-  // extend methods for all tags (Safari doesn't need this)
-  if (!F.ElementExtensions) {
-    Object.extend(methods, Element.Methods),
-    Object.extend(methods, Element.Methods.Simulated);
-  }
-
-  // extend methods for specific tags
-  if (T[tagName]) Object.extend(methods, T[tagName]);
-
-  for (var property in methods) {
-    var value = methods[property];
-    if (typeof value == 'function' && !(property in element))
-      element[property] = cache.findOrStore(value);
-  }
-
-  element._extended = Prototype.emptyFunction;
-  return element;
-};
-
-Element.extend.cache = {
-  findOrStore: function(value) {
-    return this[value] = this[value] || function() {
-      return value.apply(null, [this].concat($A(arguments)));
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+  // DOM level 2 ECMAScript Language Binding
+  Object.extend(Node, {
+    ELEMENT_NODE: 1,
+    ATTRIBUTE_NODE: 2,
+    TEXT_NODE: 3,
+    CDATA_SECTION_NODE: 4,
+    ENTITY_REFERENCE_NODE: 5,
+    ENTITY_NODE: 6,
+    PROCESSING_INSTRUCTION_NODE: 7,
+    COMMENT_NODE: 8,
+    DOCUMENT_NODE: 9,
+    DOCUMENT_TYPE_NODE: 10,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    NOTATION_NODE: 12
+  });
+}
+
+(function() {
+  var element = this.Element;
+  this.Element = function(tagName, attributes) {
+    attributes = attributes || { };
+    tagName = tagName.toLowerCase();
+    var cache = Element.cache;
+    if (Prototype.Browser.IE && (attributes.name || attributes.type)) {
+	  tagName = '<' + tagName +
+	    (attributes.name ? ' name="' + attributes.name + '"' : '') +
+	      (attributes.type ? ' type="' + attributes.type + '"' : '') + '>';
+	  delete attributes.name; delete attributes.type;
+	  return Element.writeAttribute(document.createElement(tagName), attributes);
     }
-  }
-};
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+  if (element) this.Element.prototype = element.prototype;
+}).call(window);
+
+Element.cache = { };
 
 Element.Methods = {
   visible: function(element) {
     return $(element).style.display != 'none';
   },
 
   toggle: function(element) {
     element = $(element);
     Element[Element.visible(element) ? 'hide' : 'show'](element);
     return element;
   },
 
   hide: function(element) {
-    $(element).style.display = 'none';
+    element = $(element);
+    var originalDisplay = element.style.display;
+    if (originalDisplay && originalDisplay != 'none')
+      element._originalDisplay = originalDisplay;
+    element.style.display = 'none';
     return element;
   },
 
   show: function(element) {
-    $(element).style.display = '';
+    element = $(element);
+    if (element._originalDisplay) {
+      element.style.display = element._originalDisplay;
+      element._originalDisplay = null;
+ 	} else element.style.display = '';
     return element;
   },
 
   remove: function(element) {
     element = $(element);
     element.parentNode.removeChild(element);
     return element;
   },
 
-  update: function(element, html) {
-    html = typeof html == 'undefined' ? '' : html.toString();
-    $(element).innerHTML = html.stripScripts();
-    setTimeout(function() {html.evalScripts()}, 10);
+  update: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+    content = Object.toHTML(content);
+    element.innerHTML = content.stripScripts();
+    content.evalScripts.bind(content).defer();
     return element;
   },
 
-  replace: function(element, html) {
+  replace: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
+      var range = element.ownerDocument.createRange();
+      range.selectNode(element);
+      content.evalScripts.bind(content).defer();
+      content = range.createContextualFragment(content.stripScripts());
+    }
+    element.parentNode.replaceChild(content, element);
+    return element;
+  },
+
+  insert: function(element, insertions) {
     element = $(element);
-    html = typeof html == 'undefined' ? '' : html.toString();
-    if (element.outerHTML) {
-      element.outerHTML = html.stripScripts();
-    } else {
-      var range = element.ownerDocument.createRange();
-      range.selectNodeContents(element);
-      element.parentNode.replaceChild(
-        range.createContextualFragment(html.stripScripts()), element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = {bottom:insertions};
+
+    var content, insert, tagName, childNodes;
+
+    for (var position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      insert = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+      if (position == 'top' || position == 'after') childNodes.reverse();
+      childNodes.each(insert.curry(element));
+
+      content.evalScripts.bind(content).defer();
     }
-    setTimeout(function() {html.evalScripts()}, 10);
+
     return element;
   },
 
+  wrap: function(element, wrapper, attributes) {
+    element = $(element);
+    if (Object.isElement(wrapper))
+      $(wrapper).writeAttribute(attributes || { });
+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+    else wrapper = new Element('div', wrapper);
+    if (element.parentNode)
+      element.parentNode.replaceChild(wrapper, element);
+    wrapper.appendChild(element);
+    return wrapper;
+  },
+
   inspect: function(element) {
     element = $(element);
     var result = '<' + element.tagName.toLowerCase();
     $H({'id': 'id', 'className': 'class'}).each(function(pair) {
       var property = pair.first(), attribute = pair.last();
       var value = (element[property] || '').toString();
       if (value) result += ' ' + attribute + '=' + value.inspect(true);
     });
@@ -1379,17 +1760,23 @@ Element.Methods = {
     return elements;
   },
 
   ancestors: function(element) {
     return $(element).recursivelyCollect('parentNode');
   },
 
   descendants: function(element) {
-    return $A($(element).getElementsByTagName('*')).each(Element.extend);
+    return $(element).select("*");
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
   },
 
   immediateDescendants: function(element) {
     if (!(element = $(element).firstChild)) return [];
     while (element && element.nodeType != 1) element = element.nextSibling;
     if (element) return [element].concat($(element).nextSiblings());
     return [];
   },
@@ -1403,65 +1790,112 @@ Element.Methods = {
   },
 
   siblings: function(element) {
     element = $(element);
     return element.previousSiblings().reverse().concat(element.nextSiblings());
   },
 
   match: function(element, selector) {
-    if (typeof selector == 'string')
+    if (Object.isString(selector))
       selector = new Selector(selector);
     return selector.match($(element));
   },
 
   up: function(element, expression, index) {
-    var ancestors = $(element).ancestors();
-    return expression ? Selector.findElement(ancestors, expression, index) :
-      ancestors[index || 0];
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return Object.isNumber(expression) ? ancestors[expression] :
+      Selector.findElement(ancestors, expression, index);
   },
 
   down: function(element, expression, index) {
-    var descendants = $(element).descendants();
-    return expression ? Selector.findElement(descendants, expression, index) :
-      descendants[index || 0];
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    return Object.isNumber(expression) ? element.descendants()[expression] :
+      Element.select(element, expression)[index || 0];
   },
 
   previous: function(element, expression, index) {
-    var previousSiblings = $(element).previousSiblings();
-    return expression ? Selector.findElement(previousSiblings, expression, index) :
-      previousSiblings[index || 0];
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return Object.isNumber(expression) ? previousSiblings[expression] :
+      Selector.findElement(previousSiblings, expression, index);
   },
 
   next: function(element, expression, index) {
-    var nextSiblings = $(element).nextSiblings();
-    return expression ? Selector.findElement(nextSiblings, expression, index) :
-      nextSiblings[index || 0];
-  },
-
-  getElementsBySelector: function() {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return Object.isNumber(expression) ? nextSiblings[expression] :
+      Selector.findElement(nextSiblings, expression, index);
+  },
+
+  select: function() {
     var args = $A(arguments), element = $(args.shift());
     return Selector.findChildElements(element, args);
   },
 
-  getElementsByClassName: function(element, className) {
-    return document.getElementsByClassName(className, element);
+  adjacent: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element.parentNode, args).without(element);
+  },
+
+  identify: function(element) {
+    element = $(element);
+    var id = element.readAttribute('id'), self = arguments.callee;
+    if (id) return id;
+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+    element.writeAttribute('id', id);
+    return id;
   },
 
   readAttribute: function(element, name) {
     element = $(element);
+    var t = Element._attributeTranslations.read;
+    if (t.names[name]) name = t.names[name];
+
     if (Prototype.Browser.IE) {
-      if (!element.attributes) return null;
-      var t = Element._attributeTranslations;
+      // If we're reading from a form, avoid a conflict between an attribute
+      // and a child name.
+      if (element.tagName.toUpperCase() == 'FORM' &&
+        !/^((child|parent)Node|(next|previous)Sibling)$/.test(name) &&
+          element.children[name]){
+        element = $(element.cloneNode(false));
+      }
       if (t.values[name]) return t.values[name](element, name);
-      if (t.names[name])  name = t.names[name];
-      var attribute = element.attributes[name];
-      return attribute ? attribute.nodeValue : null;
+      if (name.include(':')) {
+        return (!element.attributes || !element.attributes[name]) ? null :
+         element.attributes[name].value;
+      }
+    } else if (t.values[name]) return t.values[name](element, name);
+
+    return element.getAttribute(name);
+  },
+
+  writeAttribute: function(element, name, value) {
+    element = $(element);
+    var attributes = { }, t = Element._attributeTranslations.write;
+
+    if (typeof name == 'object') attributes = name;
+    else attributes[name] = Object.isUndefined(value) ? true : value;
+
+    for (var attr in attributes) {
+      name = t.names[attr] || attr;
+      value = attributes[attr];
+      if (t.values[name]) name = t.values[name](element, value);
+      if (value === false || value === null)
+        element.removeAttribute(name);
+      else if (value === true)
+        element.setAttribute(name, name);
+      else element.setAttribute(name, value);
     }
-    return element.getAttribute(name);
+    return element;
   },
 
   getHeight: function(element) {
     return $(element).getDimensions().height;
   },
 
   getWidth: function(element) {
     return $(element).getDimensions().width;
@@ -1469,49 +1903,38 @@ Element.Methods = {
 
   classNames: function(element) {
     return new Element.ClassNames(element);
   },
 
   hasClassName: function(element, className) {
     if (!(element = $(element))) return;
     var elementClassName = element.className;
-    if (elementClassName.length == 0) return false;
-    if (elementClassName == className ||
-        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
-      return true;
-    return false;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
   },
 
   addClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element).add(className);
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
     return element;
   },
 
   removeClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element).remove(className);
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
     return element;
   },
 
   toggleClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
-    return element;
-  },
-
-  observe: function() {
-    Event.observe.apply(Event, arguments);
-    return $A(arguments).first();
-  },
-
-  stopObserving: function() {
-    Event.stopObserving.apply(Event, arguments);
-    return $A(arguments).first();
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
   },
 
   // removes whitespace-only text node children
   cleanWhitespace: function(element) {
     element = $(element);
     var node = element.firstChild;
     while (node) {
       var nextNode = node.nextSibling;
@@ -1523,86 +1946,102 @@ Element.Methods = {
   },
 
   empty: function(element) {
     return $(element).innerHTML.blank();
   },
 
   descendantOf: function(element, ancestor) {
     element = $(element), ancestor = $(ancestor);
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (ancestor.contains)
+      return ancestor.contains(element) && ancestor !== element;
+
     while (element = element.parentNode)
       if (element == ancestor) return true;
+
     return false;
   },
 
   scrollTo: function(element) {
     element = $(element);
-    var pos = Position.cumulativeOffset(element);
+    var pos = element.cumulativeOffset();
     window.scrollTo(pos[0], pos[1]);
     return element;
   },
 
   getStyle: function(element, style) {
     element = $(element);
     style = style == 'float' ? 'cssFloat' : style.camelize();
     var value = element.style[style];
-    if (!value) {
+    if (!value || value == 'auto') {
       var css = document.defaultView.getComputedStyle(element, null);
       value = css ? css[style] : null;
     }
     if (style == 'opacity') return value ? parseFloat(value) : 1.0;
     return value == 'auto' ? null : value;
   },
 
   getOpacity: function(element) {
     return $(element).getStyle('opacity');
   },
 
-  setStyle: function(element, styles, camelized) {
+  setStyle: function(element, styles) {
     element = $(element);
-    var elementStyle = element.style;
-
+    var elementStyle = element.style, match;
+    if (Object.isString(styles)) {
+      element.style.cssText += ';' + styles;
+      return styles.include('opacity') ?
+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+    }
     for (var property in styles)
-      if (property == 'opacity') element.setOpacity(styles[property])
+      if (property == 'opacity') element.setOpacity(styles[property]);
       else
         elementStyle[(property == 'float' || property == 'cssFloat') ?
-          (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
-          (camelized ? property : property.camelize())] = styles[property];
+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
 
     return element;
   },
 
   setOpacity: function(element, value) {
     element = $(element);
     element.style.opacity = (value == 1 || value === '') ? '' :
       (value < 0.00001) ? 0 : value;
     return element;
   },
 
   getDimensions: function(element) {
     element = $(element);
-    var display = $(element).getStyle('display');
-    if (display != 'none' && display != null) // Safari bug
-      return {width: element.offsetWidth, height: element.offsetHeight};
-
-    // All *Width and *Height properties give 0 on elements with display none,
+    var display = element.getStyle('display'),
+     dimensions = { width: element.clientWidth, height: element.clientHeight };
+
+    // All *Width and *Height properties give 0 on elements with display: none,
     // so enable the element temporarily
-    var els = element.style;
-    var originalVisibility = els.visibility;
-    var originalPosition = els.position;
-    var originalDisplay = els.display;
-    els.visibility = 'hidden';
-    els.position = 'absolute';
-    els.display = 'block';
-    var originalWidth = element.clientWidth;
-    var originalHeight = element.clientHeight;
-    els.display = originalDisplay;
-    els.position = originalPosition;
-    els.visibility = originalVisibility;
-    return {width: originalWidth, height: originalHeight};
+    if (display === "none" || display === null) {
+      var els = element.style,
+       originalVisibility = els.visibility,
+       originalPosition   = els.position,
+       originalDisplay    = els.display;
+
+      els.visibility = 'hidden';
+      els.position = 'absolute';
+      els.display = 'block';
+
+      dimensions = { width: element.clientWidth, height: element.clientHeight };
+
+      els.display = originalDisplay;
+      els.position = originalPosition;
+      els.visibility = originalVisibility;
+    }
+
+    return dimensions;
   },
 
   makePositioned: function(element) {
     element = $(element);
     var pos = Element.getStyle(element, 'position');
     if (pos == 'static' || !pos) {
       element._madePositioned = true;
       element.style.position = 'relative';
@@ -1627,212 +2066,824 @@ Element.Methods = {
         element.style.right = '';
     }
     return element;
   },
 
   makeClipping: function(element) {
     element = $(element);
     if (element._overflow) return element;
-    element._overflow = element.style.overflow || 'auto';
-    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
       element.style.overflow = 'hidden';
     return element;
   },
 
   undoClipping: function(element) {
     element = $(element);
     if (!element._overflow) return element;
     element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
     element._overflow = null;
     return element;
+  },
+
+  cumulativeOffset: function(element) {
+    element = $(element);
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  positionedOffset: function(element) {
+    element = $(element);
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if (element.tagName.toUpperCase() == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p !== 'static') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return element;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    var offsets = element.positionedOffset(),
+    dimensions = element.getDimensions(),
+    top = offsets[1],
+    left = offsets[0],
+    width = dimensions.width,
+    height = dimensions.height;
+
+    Object.extend(element, {
+      _originalLeft:   left - parseFloat(element.style.left  || 0),
+      _originalTop:    top  - parseFloat(element.style.top || 0),
+      _originalWidth:  element.style.width,
+      _originalHeight: element.style.height
+    });
+
+    element.setStyle({
+      position: 'absolute',
+      top:      top + 'px',
+      left:     left + 'px',
+      width:    width + 'px',
+      height:   height + 'px'
+    });
+
+    return element;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return element;
+
+    if (!element._originalTop){
+      /* fix bizarre IE position issue with empty elements */
+      var isBuggy = element.outerHTML && element.innerHTML.blank();
+      if (isBuggy) element.innerHTML = '\x00';
+
+      Object.extend(element, {
+        _originalTop:        element.offsetTop,
+        _originalLeft:       element.offsetLeft,
+        _originalWidth:      Element.getStyle(element, 'width'),
+        _originalHeight:     Element.getStyle(element, 'height'),
+        _originalMarginTop:  Element.getStyle(element, 'marginTop'),
+        _originalMarginLeft: Element.getStyle(element, 'marginLeft')
+      });
+
+      if (isBuggy) element.innerHTML = '';
+    }
+
+    Element.setStyle(element, {
+      position:   'relative',
+      width:      element._originalWidth,
+      height:     element._originalHeight,
+      marginTop:  element._originalMarginTop,
+      marginLeft: element._originalMarginLeft
+    });
+
+    var offsets = element.positionedOffset(),
+     top  = element._originalTop  - offsets.top,
+     left = element._originalLeft - offsets.left;
+
+    var isAuto = /^(auto|)$/;
+    if (!isAuto.test(element.style.top))  top  += element._originalTop;
+    if (!isAuto.test(element.style.left)) left += element._originalLeft;
+
+    Element.setStyle(element, {
+      top:  top + 'px',
+      left: left + 'px'
+    });
+
+    return element;
+  },
+
+  getOffsetParent: function(element) {
+  	element = $(element);
+    var op = element.offsetParent, docElement = document.documentElement;
+    if (op && op != docElement) return $(op);
+
+    while ((element = element.parentNode) && element !== docElement &&
+     element !== document) {
+      if (Element.getStyle(element, 'position') != 'static')
+        return $(element);
+    }
+
+    return $(document.body);
   }
 };
 
-Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});
+Object.extend(Element.Methods, (function() {
+  function getNumericStyle(element, style) {
+    return parseFloat(Element.getStyle(element, style)) || 0;
+  }
+
+  function getStyleDiff(element, source, style) {
+    return getNumericStyle(source, style) - getNumericStyle(element, style);
+  }
+
+  function cloneDimension(element, source, dimension) {
+    var d = Element.getDimensions(source), style = { };
+    style[dimension] = d[dimension] + 'px';
+
+    var styles = $w('margin padding');
+    var sides = (dimension === 'height') ? $w('top bottom') :
+     $w('left right');
+
+    var property;
+    for (var i = 0; i < 2; i++) {
+      for (var j = 0; j < 2; j++) {
+        property = styles[i] + sides[j].capitalize();
+        style[property] = (getNumericStyle(element, property) +
+         getStyleDiff(element, source, property)) + 'px';
+      }
+    }
+    Element.setStyle(element, style);
+  }
+
+  return {
+    cumulativeScrollOffset: function(element) {
+      element = $(element);
+      var valueT = 0, valueL = 0, endElement = document;
+      var B = Prototype.Browser;
+
+      // Safari and Opera need to stop at document.body or else they'll
+      // report inaccurate values.
+      if (B.WebKit || B.Opera && opera.version() < 9.5) {
+        if ([document, document.body, document.documentElement].include(element))
+          return Element._returnOffset(0, 0);
+
+        endElement = document.body;
+      }
+
+      if (Element.getStyle(element, 'position') !== 'fixed') {
+        while ((element = element.parentNode) && element !== endElement) {
+          if (Element.getStyle(element, 'position') === 'fixed') break;
+          valueT += element.scrollTop  || 0;
+          valueL += element.scrollLeft || 0;
+        }
+      }
+      return Element._returnOffset(valueL, valueT);
+    },
+
+    cumulativeOffset: function(element) {
+      element = $(element);
+      var valueT = 0, valueL = 0;
+      do {
+        valueT += (element.offsetTop  || 0);
+        valueL += (element.offsetLeft || 0);
+      } while ((element = Element.getOffsetParent(element)) != document.body);
+
+      return Element._returnOffset(valueL, valueT);
+    },
+
+    positionedOffset: function(element) {
+      element = $(element);
+      var valueT = 0, valueL = 0;
+      do {
+        valueT += (element.offsetTop  || 0);
+        valueL += (element.offsetLeft || 0);
+        element = Element.getOffsetParent(element);
+      } while (element != document.body &&
+       Element.getStyle(element, 'position') == 'static');
+
+      return Element._returnOffset(valueL, valueT);
+    },
+
+    viewportOffset: function(forElement) {
+      forElement = $(forElement);
+      var op, element = forElement, valueT = 0, valueL = 0;
+
+      do {
+        valueT += (element.offsetTop  || 0);
+        valueL += (element.offsetLeft || 0);
+
+        // Safari fix
+        op = Element.getOffsetParent(element);
+        if (op == document.body && Element.getStyle(element,
+         'position') == 'absolute') break;
+      } while ((element = op) != document.body);
+
+      var scrollOffset = Element.cumulativeScrollOffset(forElement);
+      valueT -= scrollOffset.top;
+      valueL -= scrollOffset.left;
+
+      return Element._returnOffset(valueL, valueT);
+    },
+
+    clonePosition: function(element, source) {
+      element = $(element);
+      source = $(source);
+      var options = Object.extend({
+        setLeft:    true,
+        setTop:     true,
+        setWidth:   true,
+        setHeight:  true,
+        offsetTop:  0,
+        offsetLeft: 0
+      }, arguments[2] || { });
+
+      // find coordinate system to use
+      // delta [0,0] will do fine with position: fixed elements;
+      // position: absolute needs offsetParent deltas
+      var parent, delta = [0, 0];
+      if (Element.getStyle(element, 'position') == 'absolute') {
+        parent = Element.getOffsetParent(element);
+        delta  = Element.viewportOffset(parent);
+      }
+
+      // correct by body offsets (fixes Safari)
+      if (parent == document.body) {
+        delta[0] -= document.body.offsetLeft;
+        delta[1] -= document.body.offsetTop;
+      }
+
+      // set dimensions
+      if (options.setWidth)  cloneDimension(element, source, 'width');
+      if (options.setHeight) cloneDimension(element, source, 'height');
+
+      // find page position of source
+      var p = Element.viewportOffset(source),
+      borderOffset = ['borderLeftWidth', 'borderTopWidth'].map(
+       function(style) { return getStyleDiff(element, source, style); });
+
+      if (options.setLeft) {
+        var left = p[0] - delta[0] + borderOffset[0];
+        if (options.offsetLeft)
+          left += options.offsetLeft + getNumericStyle(element, 'paddingLeft');
+
+        element.style.left = left + 'px';
+      }
+      if (options.setTop) {
+        var top = p[1] - delta[1] + borderOffset[1];
+        if (options.offsetTop)
+          top += options.offsetTop + getNumericStyle(element, 'paddingTop');
+
+        element.style.top = top + 'px';
+      }
+      return element;
+    }
+  };
+})());
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  },
+
+  read: {
+    names: { },
+    values: {
+      _flag: function(element, attribute) {
+        return $(element).hasAttribute(attribute) ? attribute : null;
+      }
+    }
+  }
+};
+
+(function(v) {
+  Object.extend(v, {
+    disabled: v._flag,
+    checked:  v._flag,
+    readonly: v._flag,
+    multiple: v._flag
+  });
+})(Element._attributeTranslations.read.values);
 
 if (Prototype.Browser.Opera) {
-  Element.Methods._getStyle = Element.Methods.getStyle;
-  Element.Methods.getStyle = function(element, style) {
-    switch(style) {
-      case 'left':
-      case 'top':
-      case 'right':
-      case 'bottom':
-        if (Element._getStyle(element, 'position') == 'static') return null;
-      default: return Element._getStyle(element, style);
+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+    function(proceed, element, style) {
+      switch (style) {
+        case 'left': case 'top': case 'right': case 'bottom':
+          if (proceed(element, 'position') === 'static') return null;
+        case 'height': case 'width':
+          // returns '0px' for hidden elements; we want it to return null
+          if (!Element.visible(element)) return null;
+
+          // returns the border-box dimensions rather than the content-box
+          // dimensions, so we subtract padding and borders from the value
+          var dim = parseInt(proceed(element, style), 10);
+
+          if (dim !== element['offset' + style.capitalize()])
+            return dim + 'px';
+
+          var properties;
+          if (style === 'height') {
+            properties = ['border-top-width', 'padding-top',
+             'padding-bottom', 'border-bottom-width'];
+          }
+          else {
+            properties = ['border-left-width', 'padding-left',
+             'padding-right', 'border-right-width'];
+          }
+          return properties.inject(dim, function(memo, property) {
+            var val = proceed(element, property);
+            return val === null ? memo : memo - parseInt(val, 10);
+          }) + 'px';
+        default: return proceed(element, style);
+      }
+    }
+  );
+
+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+    function(proceed, element, attribute) {
+      if (attribute === 'title') return $(element).title;
+      return proceed(element, attribute);
     }
-  };
+  );
 }
+
 else if (Prototype.Browser.IE) {
+  // IE doesn't report offsets correctly for static elements, so we change them
+  // to "relative" to get the values, then change them back.
+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+    function(proceed, element) {
+      element = $(element);
+      // IE throws an error if element is not in document
+      try { element.offsetParent; }
+      catch(e) { return $(document.body); }
+
+      var position = element.getStyle('position');
+      if (position !== 'static') return proceed(element);
+      element.setStyle({ position: 'relative' });
+
+      var value = proceed(element);
+      element.setStyle({ position: position });
+      return value;
+    }
+  );
+
+  $w('positionedOffset viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+
+        var position = Element.getStyle(element, 'position');
+        if (position !== 'static') return proceed(element);
+
+        // Trigger hasLayout on the offset parent so that IE6 reports
+        // accurate offsetTop and offsetLeft values for position: fixed.
+        var offsetParent = Element.getOffsetParent(element),
+         style = { position: 'relative' };
+        if (Element.getOffsetParent(offsetParent, 'position') === 'fixed')
+          style.zoom = '1';
+
+        Element.setStyle(element, style);
+        var value = proceed(element);
+        Element.setStyle(element, { position: position});
+        return value;
+      }
+    );
+  });
+
+
   Element.Methods.getStyle = function(element, style) {
     element = $(element);
     style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
     var value = element.style[style];
     if (!value && element.currentStyle) value = element.currentStyle[style];
 
     if (style == 'opacity') {
       if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
         if (value[1]) return parseFloat(value[1]) / 100;
       return 1.0;
     }
 
     if (value == 'auto') {
       if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
-        return element['offset'+style.capitalize()] + 'px';
+        return element['offset' + style.capitalize()] + 'px';
       return null;
     }
     return value;
   };
 
   Element.Methods.setOpacity = function(element, value) {
+    function stripAlpha(filter){
+      return filter.replace(/alpha\([^\)]*\)/gi,'');
+    }
     element = $(element);
+    var currentStyle = element.currentStyle;
+    if ((currentStyle && !currentStyle.hasLayout) ||
+      (!currentStyle && element.style.zoom == 'normal'))
+        element.style.zoom = 1;
+
     var filter = element.getStyle('filter'), style = element.style;
     if (value == 1 || value === '') {
-      style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
+      (filter = stripAlpha(filter)) ?
+        style.filter = filter : style.removeAttribute('filter');
       return element;
     } else if (value < 0.00001) value = 0;
-    style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
+    style.filter = stripAlpha(filter) +
       'alpha(opacity=' + (value * 100) + ')';
     return element;
   };
 
-  // IE is missing .innerHTML support for TABLE-related elements
-  Element.Methods.update = function(element, html) {
-    element = $(element);
-    html = typeof html == 'undefined' ? '' : html.toString();
-    var tagName = element.tagName.toUpperCase();
-    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
-      var div = document.createElement('div');
-      switch (tagName) {
-        case 'THEAD':
-        case 'TBODY':
-          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
-          depth = 2;
-          break;
-        case 'TR':
-          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
-          depth = 3;
-          break;
-        case 'TD':
-          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
-          depth = 4;
-      }
-      $A(element.childNodes).each(function(node) { element.removeChild(node) });
-      depth.times(function() { div = div.firstChild });
-      $A(div.childNodes).each(function(node) { element.appendChild(node) });
-    } else {
-      element.innerHTML = html.stripScripts();
+  (function(t) {
+    t.has = { };
+    t.write.names = { };
+
+    $w('cellPadding cellSpacing colSpan rowSpan vAlign dateTime accessKey ' +
+       'tabIndex encType maxLength readOnly longDesc frameBorder').each(function(attr) {
+      var lower = attr.toLowerCase();
+      t.has[lower] = attr;
+      t.read.names[lower] = attr;
+      t.write.names[lower] = attr;
+    });
+
+    [t.write.names, t.read.names].each(function(n) {
+      Object.extend(n, {
+        'class': 'className',
+        'for': 'htmlFor'
+      });
+    });
+  })(Element._attributeTranslations);
+
+  Object.extend(Element._attributeTranslations.read.values, {
+
+    _getAttr: function(element, attribute) {
+      return element.getAttribute(attribute, 2);
+    },
+
+    _getAttrNode: function(element, attribute) {
+      var node = element.getAttributeNode(attribute);
+      return node ? node.value : "";
+    },
+
+    _getEv: function(element, attribute) {
+      attribute = element.getAttribute(attribute);
+      return attribute ? attribute.toString().slice(23, -2) : null;
+    },
+
+    style: function(element) {
+      return element.style.cssText.toLowerCase();
+    },
+
+    title: function(element) {
+      return element.title;
     }
-    setTimeout(function() { html.evalScripts() }, 10);
-    return element;
-  }
+  });
+
+  Object.extend(Element._attributeTranslations.write.values, {
+    checked: function(element, value) {
+      element.checked = !!value;
+    },
+
+    encType: function(element, value) {
+      element.getAttributeNode('encType').value = value;
+    },
+
+    style: function(element, value) {
+      element.style.cssText = value ? value : '';
+    }
+  });
+
+  (function(v) {
+    delete v.readonly;
+    Object.extend(v, {
+      href:        v._getAttr,
+      src:         v._getAttr,
+      type:        v._getAttr,
+      action:      v._getAttrNode,
+      onload:      v._getEv,
+      onunload:    v._getEv,
+      onclick:     v._getEv,
+      ondblclick:  v._getEv,
+      onmousedown: v._getEv,
+      onmouseup:   v._getEv,
+      onmouseover: v._getEv,
+      onmousemove: v._getEv,
+      onmouseout:  v._getEv,
+      onfocus:     v._getEv,
+      onblur:      v._getEv,
+      onkeypress:  v._getEv,
+      onkeydown:   v._getEv,
+      onkeyup:     v._getEv,
+      onsubmit:    v._getEv,
+      onreset:     v._getEv,
+      onselect:    v._getEv,
+      onchange:    v._getEv,
+      readOnly:    v._flag.wrap(function(proceed, element, attribute) {
+        attribute = proceed(element, attribute);
+        return attribute? 'readonly' : null;
+      })
+    });
+  })(Element._attributeTranslations.read.values);
 }
-else if (Prototype.Browser.Gecko) {
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
   Element.Methods.setOpacity = function(element, value) {
     element = $(element);
     element.style.opacity = (value == 1) ? 0.999999 :
       (value === '') ? '' : (value < 0.00001) ? 0 : value;
     return element;
   };
 }
 
-Element._attributeTranslations = {
-  names: {
-    colspan:   "colSpan",
-    rowspan:   "rowSpan",
-    valign:    "vAlign",
-    datetime:  "dateTime",
-    accesskey: "accessKey",
-    tabindex:  "tabIndex",
-    enctype:   "encType",
-    maxlength: "maxLength",
-    readonly:  "readOnly",
-    longdesc:  "longDesc"
-  },
-  values: {
-    _getAttr: function(element, attribute) {
-      return element.getAttribute(attribute, 2);
-    },
-    _flag: function(element, attribute) {
-      return $(element).hasAttribute(attribute) ? attribute : null;
-    },
-    style: function(element) {
-      return element.style.cssText.toLowerCase();
-    },
-    title: function(element) {
-      var node = element.getAttributeNode('title');
-      return node.specified ? node.nodeValue : null;
+else if (Prototype.Browser.WebKit) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+
+    if (value == 1)
+      if (element.tagName.toUpperCase() == 'IMG' && element.width) {
+        element.width++; element.width--;
+      } else try {
+        var n = document.createTextNode(' ');
+        element.appendChild(n);
+        element.removeChild(n);
+      } catch (e) { }
+
+    return element;
+  };
+
+  // Safari returns margins on body which is incorrect if the child is absolutely
+  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
+  // KHTML/WebKit only.
+  Element.Methods.cumulativeOffset = function(element) {
+    element = $(element);
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return Element._returnOffset(valueL, valueT);
+  };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+  Element.Methods.update = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+
+    content = Object.toHTML(content);
+    var tagName = element.tagName.toUpperCase();
+
+    if (tagName in Element._insertionTranslations.tags) {
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+        .each(function(node) { element.appendChild(node) });
+    }
+    else element.innerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+if (Prototype.Browser.IE) {
+  // Wrap Element#update to clean up event handlers on
+  // newly-removed elements. Prevents memory leaks in IE.
+  Element.Methods.update = Element.Methods.update.wrap(
+    function(proceed, element, contents) {
+      Element.select(element, '*').each(Event.stopObserving);
+      return proceed(element, contents);
     }
+  );
+}
+
+if ('outerHTML' in document.createElement('div')) {
+  Element.Methods.replace = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) {
+      element.parentNode.replaceChild(content, element);
+      return element;
+    }
+
+    content = Object.toHTML(content);
+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+    // Avoid outerHTML in IE because it incorrectly removes the replaced
+    // elements' child nodes.
+    if (Element._insertionTranslations.tags[tagName] || Prototype.Browser.IE) {
+      var nextSibling = element.next();
+      var fragments = Element._getContentFromAnonymousElement(tagName,
+       content.stripScripts());
+      parent.removeChild(element);
+      if (nextSibling)
+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+      else
+        fragments.each(function(node) { parent.appendChild(node) });
+    }
+    else element.outerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+Element._returnOffset = function(l, t) {
+  var result = [l, t];
+  result.left = l;
+  result.top = t;
+  return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+  if (t) {
+    div.innerHTML = t[0] + html + t[1];
+    t[2].times(function() { div = div.firstChild });
+  } else div.innerHTML = html;
+  return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+  before: function(element, node) {
+    element.parentNode.insertBefore(node, element);
+  },
+  top: function(element, node) {
+    element.insertBefore(node, element.firstChild);
+  },
+  bottom: function(element, node) {
+    element.appendChild(node);
+  },
+  after: function(element, node) {
+    element.parentNode.insertBefore(node, element.nextSibling);
+  },
+  tags: {
+    TABLE:  ['<table>',                '</table>',                   1],
+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+    SELECT: ['<select>',               '</select>',                  1]
   }
 };
 
 (function() {
-  Object.extend(this, {
-    href: this._getAttr,
-    src:  this._getAttr,
-    disabled: this._flag,
-    checked:  this._flag,
-    readonly: this._flag,
-    multiple: this._flag
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
   });
-}).call(Element._attributeTranslations.values);
+}).call(Element._insertionTranslations);
 
 Element.Methods.Simulated = {
+  // No use of $ in this function in order to keep things fast.
+  // Used by the Selector class.
   hasAttribute: function(element, attribute) {
-    var t = Element._attributeTranslations, node;
-    attribute = t.names[attribute] || attribute;
-    node = $(element).getAttributeNode(attribute);
-    return node && node.specified;
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = element.getAttributeNode(attribute);
+    return !!(node && node.specified);
   }
 };
 
-Element.Methods.ByTag = {};
+Element.Methods.ByTag = { };
 
 Object.extend(Element, Element.Methods);
 
 if (!Prototype.BrowserFeatures.ElementExtensions &&
- document.createElement('div').__proto__) {
-  window.HTMLElement = {};
-  window.HTMLElement.prototype = document.createElement('div').__proto__;
+    document.createElement('div')['__proto__']) {
+  window.HTMLElement = { };
+  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
   Prototype.BrowserFeatures.ElementExtensions = true;
 }
 
+Element.extend = (function() {
+  if (Prototype.BrowserFeatures.SpecificElementExtensions)
+    return Prototype.K;
+
+  var Methods = { }, ByTag = Element.Methods.ByTag;
+
+  var extend = Object.extend(function(element) {
+    if (!element || element._extendedByPrototype ||
+        element.nodeType != 1 || element == window) return element;
+
+    // Filter out XML nodes in IE.
+    if (!(element.ownerDocument || element).body) return element;
+
+    var methods = Object.clone(Methods),
+      tagName = element.tagName.toUpperCase(), property, value;
+
+    // extend methods for specific tags
+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+    for (property in methods) {
+      value = methods[property];
+      if (Object.isFunction(value) && !(property in element))
+        element[property] = value.methodize();
+    }
+
+    element._extendedByPrototype = Prototype.emptyFunction;
+    return element;
+
+  }, {
+    refresh: function() {
+      // extend methods for all tags (Safari doesn't need this)
+      if (!Prototype.BrowserFeatures.ElementExtensions) {
+        Object.extend(Methods, Element.Methods);
+        Object.extend(Methods, Element.Methods.Simulated);
+      }
+    }
+  });
+
+  extend.refresh();
+  return extend;
+})();
+
+
+// No use of $ in this function in order to keep things fast.
+// Used by the Selector class.
 Element.hasAttribute = function(element, attribute) {
   if (element.hasAttribute) return element.hasAttribute(attribute);
   return Element.Methods.Simulated.hasAttribute(element, attribute);
 };
 
 Element.addMethods = function(methods) {
   var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "BUTTON":   Object.clone(Form.Element.Methods),
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
   if (arguments.length == 2) {
     var tagName = methods;
     methods = arguments[1];
   }
 
-  if (!tagName) Object.extend(Element.Methods, methods || {});
+  if (!tagName) Object.extend(Element.Methods, methods || { });
   else {
-    if (tagName.constructor == Array) tagName.each(extend);
+    if (Object.isArray(tagName)) tagName.each(extend);
     else extend(tagName);
   }
 
   function extend(tagName) {
     tagName = tagName.toUpperCase();
     if (!Element.Methods.ByTag[tagName])
-      Element.Methods.ByTag[tagName] = {};
+      Element.Methods.ByTag[tagName] = { };
     Object.extend(Element.Methods.ByTag[tagName], methods);
   }
 
   function copy(methods, destination, onlyIfAbsent) {
     onlyIfAbsent = onlyIfAbsent || false;
-    var cache = Element.extend.cache;
     for (var property in methods) {
       var value = methods[property];
+      if (!Object.isFunction(value)) continue;
       if (!onlyIfAbsent || !(property in destination))
-        destination[property] = cache.findOrStore(value);
+        destination[property] = value.methodize();
     }
   }
 
   function findDOMClass(tagName) {
     var klass;
     var trans = {
       "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
       "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
@@ -1846,307 +2897,302 @@ Element.addMethods = function(methods) {
     };
     if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
     if (window[klass]) return window[klass];
     klass = 'HTML' + tagName + 'Element';
     if (window[klass]) return window[klass];
     klass = 'HTML' + tagName.capitalize() + 'Element';
     if (window[klass]) return window[klass];
 
-    window[klass] = {};
-    window[klass].prototype = document.createElement(tagName).__proto__;
+    window[klass] = { };
+    window[klass].prototype = document.createElement(tagName)['__proto__'];
     return window[klass];
   }
 
   if (F.ElementExtensions) {
     copy(Element.Methods, HTMLElement.prototype);
     copy(Element.Methods.Simulated, HTMLElement.prototype, true);
   }
 
   if (F.SpecificElementExtensions) {
     for (var tag in Element.Methods.ByTag) {
       var klass = findDOMClass(tag);
-      if (typeof klass == "undefined") continue;
+      if (Object.isUndefined(klass)) continue;
       copy(T[tag], klass.prototype);
     }
   }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+
+  if (Element.extend.refresh) Element.extend.refresh();
+  Element.cache = { };
 };
 
-var Toggle = { display: Element.toggle };
-
-/*--------------------------------------------------------------------------*/
-
-Abstract.Insertion = function(adjacency) {
-  this.adjacency = adjacency;
-}
-
-Abstract.Insertion.prototype = {
-  initialize: function(element, content) {
-    this.element = $(element);
-    this.content = content.stripScripts();
-
-    if (this.adjacency && this.element.insertAdjacentHTML) {
-      try {
-        this.element.insertAdjacentHTML(this.adjacency, this.content);
-      } catch (e) {
-        var tagName = this.element.tagName.toUpperCase();
-        if (['TBODY', 'TR'].include(tagName)) {
-          this.insertContent(this.contentFromAnonymousTable());
-        } else {
-          throw e;
-        }
-      }
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.insertContent([this.range.createContextualFragment(this.content)]);
-    }
-
-    setTimeout(function() {content.evalScripts()}, 10);
-  },
-
-  contentFromAnonymousTable: function() {
-    var div = document.createElement('div');
-    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
-    return $A(div.childNodes[0].childNodes[0].childNodes);
-  }
-}
-
-var Insertion = new Object();
-
-Insertion.Before = Class.create();
-Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment, this.element);
-    }).bind(this));
-  }
-});
-
-Insertion.Top = Class.create();
-Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
-  },
-
-  insertContent: function(fragments) {
-    fragments.reverse(false).each((function(fragment) {
-      this.element.insertBefore(fragment, this.element.firstChild);
-    }).bind(this));
-  }
-});
-
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.appendChild(fragment);
-    }).bind(this));
-  }
-});
-
-Insertion.After = Class.create();
-Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment,
-        this.element.nextSibling);
-    }).bind(this));
-  }
-});
-
-/*--------------------------------------------------------------------------*/
-
-Element.ClassNames = Class.create();
-Element.ClassNames.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-  },
-
-  _each: function(iterator) {
-    this.element.className.split(/\s+/).select(function(name) {
-      return name.length > 0;
-    })._each(iterator);
-  },
-
-  set: function(className) {
-    this.element.className = className;
-  },
-
-  add: function(classNameToAdd) {
-    if (this.include(classNameToAdd)) return;
-    this.set($A(this).concat(classNameToAdd).join(' '));
-  },
-
-  remove: function(classNameToRemove) {
-    if (!this.include(classNameToRemove)) return;
-    this.set($A(this).without(classNameToRemove).join(' '));
-  },
-
-  toString: function() {
-    return $A(this).join(' ');
+document.viewport = {
+  getDimensions: function() {
+    var dimensions = { }, B = Prototype.Browser;
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
+        (B.Opera && opera.version() < 9.5) ? document.body['client' + D] : document.documentElement['client' + D];
+    });
+    return dimensions;
+  },
+
+  getWidth: function() {
+    return this.getDimensions().width;
+  },
+
+  getHeight: function() {
+    return this.getDimensions().height;
+  },
+
+  getScrollOffsets: function() {
+    return Element._returnOffset(
+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
   }
 };
-
-Object.extend(Element.ClassNames.prototype, Enumerable);
-/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
  * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
  * license.  Please see http://www.yui-ext.com/ for more information. */
 
-var Selector = Class.create();
-
-Selector.prototype = {
+var Selector = Class.create({
   initialize: function(expression) {
     this.expression = expression.strip();
-    this.compileMatcher();
+
+    if (this.shouldUseSelectorsAPI()) {
+      this.mode = 'selectorsAPI';
+    } else if (this.shouldUseXPath()) {
+      this.mode = 'xpath';
+      this.compileXPathMatcher();
+    } else {
+      this.mode = "normal";
+      this.compileMatcher();
+    }
+
+  },
+
+  shouldUseXPath: function() {
+    if (!Prototype.BrowserFeatures.XPath) return false;
+
+    var e = this.expression;
+
+    // Safari 3 chokes on :*-of-type and :empty
+    if (Prototype.Browser.WebKit &&
+     (e.include("-of-type") || e.include(":empty")))
+      return false;
+
+    // XPath can't do namespaced attributes, nor can it read
+    // the "checked" property from DOM nodes
+    if ((/(\[[\w-]*?:|:checked)/).test(e))
+      return false;
+
+    return true;
+  },
+
+  shouldUseSelectorsAPI: function() {
+    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
+
+    if (!Selector._div) Selector._div = new Element('div');
+
+    // Make sure the browser treats the selector as valid. Test on an
+    // isolated element to minimize cost of this check.
+    try {
+      Selector._div.querySelector(this.expression);
+    } catch(e) {
+      return false;
+    }
+
+    return true;
   },
 
   compileMatcher: function() {
-    // Selectors with namespaced attributes can't use the XPath version
-    if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
-      return this.compileXPathMatcher();
-
     var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
         c = Selector.criteria, le, p, m;
 
     if (Selector._cache[e]) {
-      this.matcher = Selector._cache[e]; return;
+      this.matcher = Selector._cache[e];
+      return;
     }
+
     this.matcher = ["this.matcher = function(root) {",
                     "var r = root, h = Selector.handlers, c = false, n;"];
 
     while (e && le != e && (/\S/).test(e)) {
       le = e;
       for (var i in ps) {
         p = ps[i];
         if (m = e.match(p)) {
-          this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
-    	      new Template(c[i]).evaluate(m));
+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+            new Template(c[i]).evaluate(m));
           e = e.replace(m[0], '');
           break;
         }
       }
     }
 
     this.matcher.push("return h.unique(n);\n}");
     eval(this.matcher.join('\n'));
     Selector._cache[this.expression] = this.matcher;
   },
 
   compileXPathMatcher: function() {
     var e = this.expression, ps = Selector.patterns,
-        x = Selector.xpath, le,  m;
+        x = Selector.xpath, le, m;
 
     if (Selector._cache[e]) {
       this.xpath = Selector._cache[e]; return;
     }
 
     this.matcher = ['.//*'];
     while (e && le != e && (/\S/).test(e)) {
       le = e;
       for (var i in ps) {
         if (m = e.match(ps[i])) {
-          this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
             new Template(x[i]).evaluate(m));
           e = e.replace(m[0], '');
           break;
         }
       }
     }
 
     this.xpath = this.matcher.join('');
     Selector._cache[this.expression] = this.xpath;
   },
 
   findElements: function(root) {
     root = root || document;
-    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
-    return this.matcher(root);
+    var e = this.expression, results;
+
+    switch (this.mode) {
+      case 'selectorsAPI':
+        // querySelectorAll queries document-wide, then filters to descendants
+        // of the context element. That's not what we want.
+        // Add an explicit context to the selector if necessary.
+        if (root !== document) {
+          var oldId = root.id, id = $(root).identify();
+          e = "#" + id + " " + e;
+        }
+
+        results = $A(root.querySelectorAll(e)).map(Element.extend);
+        root.id = oldId;
+
+        return results;
+      case 'xpath':
+        return document._getElementsByXPath(this.xpath, root);
+      default:
+       return this.matcher(root);
+    }
   },
 
   match: function(element) {
-    return this.findElements(document).include(element);
+    this.tokens = [];
+
+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+    var le, p, m;
+
+    while (e && le !== e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          // use the Selector.assertions methods unless the selector
+          // is too complex.
+          if (as[i]) {
+            this.tokens.push([i, Object.clone(m)]);
+            e = e.replace(m[0], '');
+          } else {
+            // reluctantly do a document-wide search
+            // and look for a match in the array
+            return this.findElements(document).include(element);
+          }
+        }
+      }
+    }
+
+    var match = true, name, matches;
+    for (var i = 0, token; token = this.tokens[i]; i++) {
+      name = token[0], matches = token[1];
+      if (!Selector.assertions[name](element, matches)) {
+        match = false; break;
+      }
+    }
+
+    return match;
   },
 
   toString: function() {
     return this.expression;
   },
 
   inspect: function() {
     return "#<Selector:" + this.expression.inspect() + ">";
   }
-};
+});
 
 Object.extend(Selector, {
-  _cache: {},
+  _cache: { },
 
   xpath: {
     descendant:   "//*",
     child:        "/*",
     adjacent:     "/following-sibling::*[1]",
     laterSibling: '/following-sibling::*',
     tagName:      function(m) {
       if (m[1] == '*') return '';
       return "[local-name()='" + m[1].toLowerCase() +
              "' or local-name()='" + m[1].toUpperCase() + "']";
     },
     className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
     id:           "[@id='#{1}']",
-    attrPresence: "[@#{1}]",
+    attrPresence: function(m) {
+      m[1] = m[1].toLowerCase();
+      return new Template("[@#{1}]").evaluate(m);
+    },
     attr: function(m) {
+      m[1] = m[1].toLowerCase();
       m[3] = m[5] || m[6];
       return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
     },
     pseudo: function(m) {
       var h = Selector.xpath.pseudos[m[1]];
       if (!h) return '';
-      if (typeof h === 'function') return h(m);
+      if (Object.isFunction(h)) return h(m);
       return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
     },
     operators: {
       '=':  "[@#{1}='#{3}']",
       '!=': "[@#{1}!='#{3}']",
       '^=': "[starts-with(@#{1}, '#{3}')]",
       '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
       '*=': "[contains(@#{1}, '#{3}')]",
       '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
       '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
     },
     pseudos: {
       'first-child': '[not(preceding-sibling::*)]',
       'last-child':  '[not(following-sibling::*)]',
       'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
-      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
       'checked':     "[@checked]",
-      'disabled':    "[@disabled]",
-      'enabled':     "[not(@disabled)]",
+      'disabled':    "[(@disabled) and (@type!='hidden')]",
+      'enabled':     "[not(@disabled) and (@type!='hidden')]",
       'not': function(m) {
         var e = m[6], p = Selector.patterns,
-            x = Selector.xpath, le, m, v;
+            x = Selector.xpath, le, v;
 
         var exclusion = [];
         while (e && le != e && (/\S/).test(e)) {
           le = e;
           for (var i in p) {
             if (m = e.match(p[i])) {
-              v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
               exclusion.push("(" + v.substring(1, v.length - 1) + ")");
               e = e.replace(m[0], '');
               break;
             }
           }
         }
         return "[not(" + exclusion.join(" and ") + ")]";
       },
@@ -2186,25 +3232,25 @@ Object.extend(Selector, {
           return new Template(predicate).evaluate({
             fragment: fragment, a: a, b: b });
         }
       }
     }
   },
 
   criteria: {
-    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
-    className:    'n = h.className(n, r, "#{1}", c); c = false;',
-    id:           'n = h.id(n, r, "#{1}", c);        c = false;',
-    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
+    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
     attr: function(m) {
       m[3] = (m[5] || m[6]);
-      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
     },
-    pseudo:       function(m) {
+    pseudo: function(m) {
       if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
       return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
     },
     descendant:   'c = "descendant";',
     child:        'c = "child";',
     adjacent:     'c = "adjacent";',
     laterSibling: 'c = "laterSibling";'
   },
@@ -2216,83 +3262,109 @@ Object.extend(Selector, {
     child:        /^\s*>\s*/,
     adjacent:     /^\s*\+\s*/,
     descendant:   /^\s/,
 
     // selectors follow
     tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
     id:           /^#([\w\-\*]+)(\b|$)/,
     className:    /^\.([\w\-\*]+)(\b|$)/,
-    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s)/,
-    attrPresence: /^\[([\w]+)\]/,
-    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
+    pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  // for Selector.match and Element#match
+  assertions: {
+    tagName: function(element, matches) {
+      return matches[1].toUpperCase() == element.tagName.toUpperCase();
+    },
+
+    className: function(element, matches) {
+      return Element.hasClassName(element, matches[1]);
+    },
+
+    id: function(element, matches) {
+      return element.id === matches[1];
+    },
+
+    attrPresence: function(element, matches) {
+      return Element.hasAttribute(element, matches[1]);
+    },
+
+    attr: function(element, matches) {
+      var nodeValue = Element.readAttribute(element, matches[1]);
+      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+    }
   },
 
   handlers: {
     // UTILITY FUNCTIONS
     // joins two collections
     concat: function(a, b) {
       for (var i = 0, node; node = b[i]; i++)
         a.push(node);
       return a;
     },
 
     // marks an array of nodes for counting
     mark: function(nodes) {
+      var _true = Prototype.emptyFunction;
       for (var i = 0, node; node = nodes[i]; i++)
-        node._counted = true;
+        node._countedByPrototype = _true;
       return nodes;
     },
 
     unmark: function(nodes) {
       for (var i = 0, node; node = nodes[i]; i++)
-        node._counted = undefined;
+        node._countedByPrototype = undefined;
       return nodes;
     },
 
     // mark each child node with its position (for nth calls)
     // "ofType" flag indicates whether we're indexing for nth-of-type
     // rather than nth-child
     index: function(parentNode, reverse, ofType) {
-      parentNode._counted = true;
+      parentNode._countedByPrototype = Prototype.emptyFunction;
       if (reverse) {
         for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
-          node = nodes[i];
-          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+          var node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
         }
       } else {
         for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
-          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
       }
     },
 
     // filters out duplicates and extends all nodes
     unique: function(nodes) {
       if (nodes.length == 0) return nodes;
       var results = [], n;
       for (var i = 0, l = nodes.length; i < l; i++)
-        if (!(n = nodes[i])._counted) {
-          n._counted = true;
+        if (!(n = nodes[i])._countedByPrototype) {
+          n._countedByPrototype = Prototype.emptyFunction;
           results.push(Element.extend(n));
         }
       return Selector.handlers.unmark(results);
     },
 
     // COMBINATOR FUNCTIONS
     descendant: function(nodes) {
       var h = Selector.handlers;
       for (var i = 0, results = [], node; node = nodes[i]; i++)
         h.concat(results, node.getElementsByTagName('*'));
       return results;
     },
 
     child: function(nodes) {
       var h = Selector.handlers;
       for (var i = 0, results = [], node; node = nodes[i]; i++) {
-        for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+        for (var j = 0, child; child = node.childNodes[j]; j++)
           if (child.nodeType == 1 && child.tagName != '!') results.push(child);
       }
       return results;
     },
 
     adjacent: function(nodes) {
       for (var i = 0, results = [], node; node = nodes[i]; i++) {
         var next = this.nextElementSibling(node);
@@ -2305,49 +3377,62 @@ Object.extend(Selector, {
       var h = Selector.handlers;
       for (var i = 0, results = [], node; node = nodes[i]; i++)
         h.concat(results, Element.nextSiblings(node));
       return results;
     },
 
     nextElementSibling: function(node) {
       while (node = node.nextSibling)
-	      if (node.nodeType == 1) return node;
+        if (node.nodeType == 1) return node;
       return null;
     },
 
     previousElementSibling: function(node) {
       while (node = node.previousSibling)
         if (node.nodeType == 1) return node;
       return null;
     },
 
     // TOKEN FUNCTIONS
     tagName: function(nodes, root, tagName, combinator) {
-      tagName = tagName.toUpperCase();
+      var uTagName = tagName.toUpperCase();
       var results = [], h = Selector.handlers;
       if (nodes) {
         if (combinator) {
           // fastlane for ordinary descendant combinators
           if (combinator == "descendant") {
             for (var i = 0, node; node = nodes[i]; i++)
               h.concat(results, node.getElementsByTagName(tagName));
             return results;
           } else nodes = this[combinator](nodes);
           if (tagName == "*") return nodes;
         }
         for (var i = 0, node; node = nodes[i]; i++)
-          if (node.tagName.toUpperCase() == tagName) results.push(node);
+          if (node.tagName.toUpperCase() === uTagName) results.push(node);
         return results;
       } else return root.getElementsByTagName(tagName);
     },
 
     id: function(nodes, root, id, combinator) {
       var targetNode = $(id), h = Selector.handlers;
-      if (!nodes && root == document) return targetNode ? [targetNode] : [];
+      if (!targetNode) {
+        // IE doesn't find elements by ID if they're not attached to the
+        // document.
+        if (Prototype.Browser.IE && (root.sourceIndex < 1 || root === document)) {
+          var nodes = root.getElementsByTagName('*');
+          for (var i = 0, node; node = nodes[i]; i++) {
+            if (node[id] = id) {
+              targetNode = node; break;
+            }
+          } if (!targetNode) return [];
+        } else return [];
+      }
+
+      if (!nodes && root === document) return [targetNode];
       if (nodes) {
         if (combinator) {
           if (combinator == 'child') {
             for (var i = 0, node; node = nodes[i]; i++)
               if (targetNode.parentNode == node) return [targetNode];
           } else if (combinator == 'descendant') {
             for (var i = 0, node; node = nodes[i]; i++)
               if (Element.descendantOf(targetNode, node)) return [targetNode];
@@ -2376,25 +3461,28 @@ Object.extend(Selector, {
         nodeClassName = node.className;
         if (nodeClassName.length == 0) continue;
         if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
           results.push(node);
       }
       return results;
     },
 
-    attrPresence: function(nodes, root, attr) {
+    attrPresence: function(nodes, root, attr, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
       var results = [];
       for (var i = 0, node; node = nodes[i]; i++)
         if (Element.hasAttribute(node, attr)) results.push(node);
       return results;
     },
 
-    attr: function(nodes, root, attr, value, operator) {
+    attr: function(nodes, root, attr, value, operator, combinator) {
       if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
       var handler = Selector.operators[operator], results = [];
       for (var i = 0, node; node = nodes[i]; i++) {
         var nodeValue = Element.readAttribute(node, attr);
         if (nodeValue === null) continue;
         if (handler(nodeValue, value)) results.push(node);
       }
       return results;
     },
@@ -2463,17 +3551,17 @@ Object.extend(Selector, {
     // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
     nth: function(nodes, formula, root, reverse, ofType) {
       if (nodes.length == 0) return [];
       if (formula == 'even') formula = '2n+0';
       if (formula == 'odd')  formula = '2n+1';
       var h = Selector.handlers, results = [], indexed = [], m;
       h.mark(nodes);
       for (var i = 0, node; node = nodes[i]; i++) {
-        if (!node.parentNode._counted) {
+        if (!node.parentNode._countedByPrototype) {
           h.index(node.parentNode, reverse, ofType);
           indexed.push(node.parentNode);
         }
       }
       if (formula.match(/^\d+$/)) { // just a number
         formula = Number(formula);
         for (var i = 0, node; node = nodes[i]; i++)
           if (node.nodeIndex == formula) results.push(node);
@@ -2490,35 +3578,36 @@ Object.extend(Selector, {
       h.unmark(nodes);
       h.unmark(indexed);
       return results;
     },
 
     'empty': function(nodes, value, root) {
       for (var i = 0, results = [], node; node = nodes[i]; i++) {
         // IE treats comments as element nodes
-        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        if (node.tagName == '!' || node.firstChild) continue;
         results.push(node);
       }
       return results;
     },
 
     'not': function(nodes, selector, root) {
       var h = Selector.handlers, selectorType, m;
       var exclusions = new Selector(selector).findElements(root);
       h.mark(exclusions);
       for (var i = 0, results = [], node; node = nodes[i]; i++)
-        if (!node._counted) results.push(node);
+        if (!node._countedByPrototype) results.push(node);
       h.unmark(exclusions);
       return results;
     },
 
     'enabled': function(nodes, value, root) {
       for (var i = 0, results = [], node; node = nodes[i]; i++)
-        if (!node.disabled) results.push(node);
+        if (!node.disabled && (!node.type || node.type !== 'hidden'))
+          results.push(node);
       return results;
     },
 
     'disabled': function(nodes, value, root) {
       for (var i = 0, results = [], node; node = nodes[i]; i++)
         if (node.disabled) results.push(node);
       return results;
     },
@@ -2528,84 +3617,147 @@ Object.extend(Selector, {
         if (node.checked) results.push(node);
       return results;
     }
   },
 
   operators: {
     '=':  function(nv, v) { return nv == v; },
     '!=': function(nv, v) { return nv != v; },
-    '^=': function(nv, v) { return nv.startsWith(v); },
+    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
+    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
+    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
     '$=': function(nv, v) { return nv.endsWith(v); },
     '*=': function(nv, v) { return nv.include(v); },
     '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
-    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
+     '-').include('-' + (v || "").toUpperCase() + '-'); }
+  },
+
+  split: function(expression) {
+    var expressions = [];
+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    return expressions;
   },
 
   matchElements: function(elements, expression) {
-    var matches = new Selector(expression).findElements(), h = Selector.handlers;
+    var matches = $$(expression), h = Selector.handlers;
     h.mark(matches);
     for (var i = 0, results = [], element; element = elements[i]; i++)
-      if (element._counted) results.push(element);
+      if (element._countedByPrototype) results.push(element);
     h.unmark(matches);
     return results;
   },
 
   findElement: function(elements, expression, index) {
-    if (typeof expression == 'number') {
+    if (Object.isNumber(expression)) {
       index = expression; expression = false;
     }
     return Selector.matchElements(elements, expression || '*')[index || 0];
   },
 
   findChildElements: function(element, expressions) {
-    var exprs = expressions.join(','), expressions = [];
-    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
-      expressions.push(m[1].strip());
-    });
+    expressions = Selector.split(expressions.join(','));
     var results = [], h = Selector.handlers;
     for (var i = 0, l = expressions.length, selector; i < l; i++) {
       selector = new Selector(expressions[i].strip());
       h.concat(results, selector.findElements(element));
     }
     return (l > 1) ? h.unique(results) : results;
   }
 });
 
+if (Prototype.Browser.IE) {
+  Object.extend(Selector.handlers, {
+    // IE returns comment nodes on getElementsByTagName("*").
+    // Filter them out.
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        if (node.tagName !== "!") a.push(node);
+      return a;
+    },
+
+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node.removeAttribute('_countedByPrototype');
+      return nodes;
+    }
+  });
+}
+
 function $$() {
   return Selector.findChildElements(document, $A(arguments));
 }
 var Form = {
   reset: function(form) {
     $(form).reset();
     return form;
   },
 
-  serializeElements: function(elements, getHash) {
-    var data = elements.inject({}, function(result, element) {
-      if (!element.disabled && element.name) {
-        var key = element.name, value = $(element).getValue();
-        if (value != null) {
-         	if (key in result) {
-            if (result[key].constructor != Array) result[key] = [result[key]];
-            result[key].push(value);
-          }
-          else result[key] = value;
+  serializeElements: function(elements, options) {
+    if (typeof options !== 'object') options = { hash: !!options };
+    else if (Object.isUndefined(options.hash)) options.hash = true;
+
+    var key, value, type, isImageType, isSubmitButton, submitSerialized;
+    var submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
+      element = $(element);
+      key     = element.name;
+      value   = element.getValue();
+      type    = element.type;
+
+      isImageType    = type === 'image';
+      isSubmitButton = (type === 'submit' || isImageType);
+
+      // Null values don't get serialized
+      if (value === null) return result;
+      // Disabled elements don't get serialized
+      if (element.disabled) return result;
+      // <input type="file|reset" /> doesn't get serialized
+      if (type === 'file' || type === 'reset') return result;
+      // Non-active submit buttons don't get serialized
+      if (isSubmitButton &&
+       (submit === false || submitSerialized ||
+       (submit && !(key === submit || element === submit))))
+        return result;
+
+      if (isSubmitButton) {
+        submitSerialized = true;
+        if (isImageType) {
+          var prefix = key ? key + '.' : '',
+           x = options.x || 0, y = options.y || 0;
+
+          result[prefix + 'x'] = x;
+          result[prefix + 'y'] = y;
+          return result;
         }
       }
+
+      else if (!key) return result;
+
+      if (key in result) {
+        // a key is already present; construct an array of values
+        if (!Object.isArray(result[key])) result[key] = [result[key]];
+        result[key].push(value);
+      } else result[key] = value;
+
       return result;
     });
 
-    return getHash ? data : Hash.toQueryString(data);
+    return options.hash ? data : Object.toQueryString(data);
   }
 };
 
 Form.Methods = {
-  serialize: function(form, getHash) {
-    return Form.serializeElements(Form.getElements(form), getHash);
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
   },
 
   getElements: function(form) {
     return $A($(form).getElementsByTagName('*')).inject([],
       function(elements, child) {
         if (Form.Element.Serializers[child.tagName.toLowerCase()])
           elements.push(Element.extend(child));
         return elements;
@@ -2637,153 +3789,178 @@ Form.Methods = {
 
   enable: function(form) {
     form = $(form);
     Form.getElements(form).invoke('enable');
     return form;
   },
 
   findFirstElement: function(form) {
-    return $(form).getElements().find(function(element) {
-      return element.type != 'hidden' && !element.disabled &&
-        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    var elements = $(form).getElements().findAll(function(element) {
+      return 'hidden' != element.type && !element.disabled;
+    });
+    var firstByIndex = elements.findAll(function(element) {
+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+    }).sortBy(function(element) { return element.tabIndex }).first();
+
+    return firstByIndex ? firstByIndex : elements.find(function(element) {
+      return ['button', 'input', 'select', 'textarea'].include(element.tagName.toLowerCase());
     });
   },
 
   focusFirstElement: function(form) {
     form = $(form);
     form.findFirstElement().activate();
     return form;
   },
 
   request: function(form, options) {
-    form = $(form), options = Object.clone(options || {});
-
-    var params = options.parameters;
+    form = $(form), options = Object.clone(options || { });
+
+    var params = options.parameters, action = form.readAttribute('action') || '';
+    if (action.blank()) action = window.location.href;
     options.parameters = form.serialize(true);
 
     if (params) {
-      if (typeof params == 'string') params = params.toQueryParams();
+      if (Object.isString(params)) params = params.toQueryParams();
       Object.extend(options.parameters, params);
     }
 
     if (form.hasAttribute('method') && !options.method)
       options.method = form.method;
 
-    return new Ajax.Request(form.readAttribute('action'), options);
+    return new Ajax.Request(action, options);
   }
-}
-
-Object.extend(Form, Form.Methods);
+};
 
 /*--------------------------------------------------------------------------*/
 
 Form.Element = {
   focus: function(element) {
     $(element).focus();
     return element;
   },
 
   select: function(element) {
     $(element).select();
     return element;
   }
-}
+};
 
 Form.Element.Methods = {
   serialize: function(element) {
     element = $(element);
     if (!element.disabled && element.name) {
       var value = element.getValue();
       if (value != undefined) {
-        var pair = {};
+        var pair = { };
         pair[element.name] = value;
-        return Hash.toQueryString(pair);
+        return Object.toQueryString(pair);
       }
     }
     return '';
   },
 
   getValue: function(element) {
-    element = $(element);
-    var method = element.tagName.toLowerCase();
-    return Form.Element.Serializers[method](element);
+    if (!(element = $(element))) return null;
+    var method = element.tagName.toLowerCase(), s = Form.Element.Serializers;
+    return s[method]? s[method](element) : null;
+  },
+
+  setValue: function(element, value) {
+    if (!(element = $(element))) return null;
+    var method = element.tagName.toLowerCase(), s = Form.Element.Serializers;
+    if (s[method]) s[method](element, value);
+    return element;
   },
 
   clear: function(element) {
     $(element).value = '';
     return element;
   },
 
   present: function(element) {
     return $(element).value != '';
   },
 
   activate: function(element) {
     element = $(element);
     try {
       element.focus();
       if (element.select && (element.tagName.toLowerCase() != 'input' ||
-        !['button', 'reset', 'submit'].include(element.type)))
+          !['button', 'image', 'reset', 'submit'].include(element.type)))
         element.select();
-    } catch (e) {}
+    } catch (e) { }
     return element;
   },
 
   disable: function(element) {
     element = $(element);
-    element.blur();
     element.disabled = true;
     return element;
   },
 
   enable: function(element) {
     element = $(element);
     element.disabled = false;
     return element;
   }
-}
-
-Object.extend(Form.Element, Form.Element.Methods);
-Object.extend(Element.Methods.ByTag, {
-  "FORM":     Object.clone(Form.Methods),
-  "INPUT":    Object.clone(Form.Element.Methods),
-  "SELECT":   Object.clone(Form.Element.Methods),
-  "TEXTAREA": Object.clone(Form.Element.Methods)
-});
+};
 
 /*--------------------------------------------------------------------------*/
 
 var Field = Form.Element;
-var $F = Form.Element.getValue;
+var $F = Form.Element.Methods.getValue;
 
 /*--------------------------------------------------------------------------*/
 
 Form.Element.Serializers = {
-  input: function(element) {
+  input: function(element, value) {
     switch (element.type.toLowerCase()) {
       case 'checkbox':
       case 'radio':
-        return Form.Element.Serializers.inputSelector(element);
+        return Form.Element.Serializers.inputSelector(element, value);
       default:
-        return Form.Element.Serializers.textarea(element);
+        return Form.Element.Serializers.textarea(element, value);
     }
   },
 
-  inputSelector: function(element) {
-    return element.checked ? element.value : null;
-  },
-
-  textarea: function(element) {
-    return element.value;
-  },
-
-  select: function(element) {
-    return this[element.type == 'select-one' ?
-      'selectOne' : 'selectMany'](element);
+  inputSelector: function(element, value) {
+    if (Object.isUndefined(value)) return element.checked ? element.value : null;
+    else element.checked = !!value;
+  },
+
+  button: function(element, value){
+    if (Object.isUndefined(value)) return element.innerHTML;
+    else element.innerHTML = value;
+  },
+
+  textarea: function(element, value) {
+    if (Object.isUndefined(value)) return element.value;
+    else element.value = value;
+  },
+
+  select: function(element, index) {
+    if (Object.isUndefined(index))
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, value, single = !Object.isArray(index);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        value = this.optionValue(opt);
+        if (single) {
+          if (value == index) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = index.include(value);
+      }
+    }
   },
 
   selectOne: function(element) {
     var index = element.selectedIndex;
     return index >= 0 ? this.optionValue(element.options[index]) : null;
   },
 
   selectMany: function(element) {
@@ -2796,64 +3973,52 @@ Form.Element.Serializers = {
     }
     return values;
   },
 
   optionValue: function(opt) {
     // extend element because hasAttribute may not be native
     return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
   }
-}
+};
 
 /*--------------------------------------------------------------------------*/
 
-Abstract.TimedObserver = function() {}
-Abstract.TimedObserver.prototype = {
-  initialize: function(element, frequency, callback) {
-    this.frequency = frequency;
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
     this.element   = $(element);
-    this.callback  = callback;
-
     this.lastValue = this.getValue();
-    this.registerCallback();
-  },
-
-  registerCallback: function() {
-    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
-  },
-
-  onTimerEvent: function() {
+  },
+
+  execute: function() {
     var value = this.getValue();
-    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
-      ? this.lastValue != value : String(this.lastValue) != String(value));
-    if (changed) {
+    if (Object.isString(this.lastValue) && Object.isString(value) ?
+        this.lastValue != value : String(this.lastValue) != String(value)) {
       this.callback(this.element, value);
       this.lastValue = value;
     }
   }
-}
-
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
   getValue: function() {
     return Form.Element.getValue(this.element);
   }
 });
 
-Form.Observer = Class.create();
-Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+Form.Observer = Class.create(Abstract.TimedObserver, {
   getValue: function() {
     return Form.serialize(this.element);
   }
 });
 
 /*--------------------------------------------------------------------------*/
 
-Abstract.EventObserver = function() {}
-Abstract.EventObserver.prototype = {
+Abstract.EventObserver = Class.create({
   initialize: function(element, callback) {
     this.element  = $(element);
     this.callback = callback;
 
     this.lastValue = this.getValue();
     if (this.element.tagName.toLowerCase() == 'form')
       this.registerFormCallbacks();
     else
@@ -2864,159 +4029,499 @@ Abstract.EventObserver.prototype = {
     var value = this.getValue();
     if (this.lastValue != value) {
       this.callback(this.element, value);
       this.lastValue = value;
     }
   },
 
   registerFormCallbacks: function() {
-    Form.getElements(this.element).each(this.registerCallback.bind(this));
+    Form.getElements(this.element).each(this.registerCallback, this);
   },
 
   registerCallback: function(element) {
     if (element.type) {
       switch (element.type.toLowerCase()) {
         case 'checkbox':
         case 'radio':
           Event.observe(element, 'click', this.onElementEvent.bind(this));
           break;
         default:
           Event.observe(element, 'change', this.onElementEvent.bind(this));
           break;
       }
     }
   }
-}
-
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
   getValue: function() {
     return Form.Element.getValue(this.element);
   }
 });
 
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+Form.EventObserver = Class.create(Abstract.EventObserver, {
   getValue: function() {
     return Form.serialize(this.element);
   }
 });
-if (!window.Event) {
-  var Event = new Object();
-}
+if (!window.Event) var Event = { };
 
 Object.extend(Event, {
   KEY_BACKSPACE: 8,
   KEY_TAB:       9,
   KEY_RETURN:   13,
   KEY_ESC:      27,
   KEY_LEFT:     37,
   KEY_UP:       38,
   KEY_RIGHT:    39,
   KEY_DOWN:     40,
   KEY_DELETE:   46,
   KEY_HOME:     36,
   KEY_END:      35,
   KEY_PAGEUP:   33,
   KEY_PAGEDOWN: 34,
-
-  element: function(event) {
-    return $(event.target || event.srcElement);
-  },
-
-  isLeftClick: function(event) {
-    return (((event.which) && (event.which == 1)) ||
-            ((event.button) && (event.button == 1)));
-  },
-
-  pointerX: function(event) {
-    return event.pageX || (event.clientX +
-      (document.documentElement.scrollLeft || document.body.scrollLeft));
-  },
-
-  pointerY: function(event) {
-    return event.pageY || (event.clientY +
-      (document.documentElement.scrollTop || document.body.scrollTop));
-  },
-
-  stop: function(event) {
-    if (event.preventDefault) {
+  KEY_INSERT:   45,
+
+  cache: { },
+
+  relatedTarget: function(event) {
+    var element;
+    switch(event.type) {
+      case 'mouseover': element = event.fromElement; break;
+      case 'mouseout':  element = event.toElement;   break;
+      default: return null;
+    }
+    return Element.extend(element);
+  }
+});
+
+Event.Methods = (function() {
+  var isButton;
+
+  if (Prototype.Browser.IE) {
+    var buttonMap = { 0: 1, 1: 4, 2: 2 };
+    isButton = function(event, code) {
+      return event.button == buttonMap[code];
+    };
+
+  } else if (Prototype.Browser.WebKit) {
+    isButton = function(event, code) {
+      switch (code) {
+        case 0: return event.which == 1 && !event.metaKey;
+        case 1: return event.which == 1 && event.metaKey;
+        default: return false;
+      }
+    };
+
+  } else {
+    isButton = function(event, code) {
+      return event.which ? (event.which === code + 1) : (event.button === code);
+    };
+  }
+
+  return {
+    isLeftClick:   function(event) { return isButton(event, 0) },
+    isMiddleClick: function(event) { return isButton(event, 1) },
+    isRightClick:  function(event) { return isButton(event, 2) },
+
+    element: function(event) {
+      event = Event.extend(event);
+      var node = event.target, currentTarget = event.currentTarget, type = event.type;
+
+      if (currentTarget && currentTarget.tagName) {
+        // Firefox screws up the "click" event when moving between radio buttons
+        // via arrow keys. It also screws up the "load" and "error" events on images,
+        // reporting the document as the target instead of the original image.
+        if (['load', 'error'].include(type) ||
+         (currentTarget.tagName.toUpperCase() === "INPUT" && currentTarget.type === "radio" && type === "click"))
+          node = currentTarget;
+      }
+
+      return Element.extend(node && node.nodeType == Node.TEXT_NODE ?
+       node.parentNode : node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      if (!expression) return element;
+      var elements = [element].concat(element.ancestors());
+      return Selector.findElement(elements, expression, 0);
+    },
+
+    pointer: function(event) {
+      var docElement = document.documentElement,
+      body = document.body || { scrollLeft: 0, scrollTop: 0 };
+      return {
+        x: event.pageX || (event.clientX +
+          (docElement.scrollLeft || body.scrollLeft) -
+          (docElement.clientLeft || 0)),
+        y: event.pageY || (event.clientY +
+          (docElement.scrollTop || body.scrollTop) -
+          (docElement.clientTop || 0))
+      };
+    },
+
+    pointerX: function(event) { return Event.pointer(event).x },
+    pointerY: function(event) { return Event.pointer(event).y },
+
+    stop: function(event) {
+      Event.extend(event);
       event.preventDefault();
       event.stopPropagation();
-    } else {
-      event.returnValue = false;
-      event.cancelBubble = true;
-    }
-  },
-
-  // find the first node with the given tagName, starting from the
-  // node the event was triggered on; traverses the DOM upwards
-  findElement: function(event, tagName) {
-    var element = Event.element(event);
-    while (element.parentNode && (!element.tagName ||
-        (element.tagName.toUpperCase() != tagName.toUpperCase())))
-      element = element.parentNode;
-    return element;
-  },
-
-  observers: false,
-
-  _observeAndCache: function(element, name, observer, useCapture) {
-    if (!this.observers) this.observers = [];
-    if (element.addEventListener) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.addEventListener(name, observer, useCapture);
-    } else if (element.attachEvent) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.attachEvent('on' + name, observer);
+      event.stopped = true;
     }
-  },
-
-  unloadCache: function() {
-    if (!Event.observers) return;
-    for (var i = 0, length = Event.observers.length; i < length; i++) {
-      Event.stopObserving.apply(this, Event.observers[i]);
-      Event.observers[i][0] = null;
+  };
+})();
+
+Event.extend = (function() {
+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+    m[name] = Event.Methods[name].methodize();
+    return m;
+  });
+
+  if (Prototype.Browser.IE) {
+    Object.extend(methods, {
+      stopPropagation: function() { this.cancelBubble = true },
+      preventDefault:  function() { this.returnValue = false },
+      inspect: function() { return "[object Event]" }
+    });
+
+    return function(event) {
+      if (!event) return false;
+      if (event._extendedByPrototype) return event;
+
+      var pointer = Event.pointer(event);
+      Object.extend(event, {
+        _extendedByPrototype: Prototype.emptyFunction,
+        target:        Element.extend(event.srcElement),
+        relatedTarget: Event.relatedTarget(event),
+        pageX:         pointer.x,
+        pageY:         pointer.y
+      });
+      return Object.extend(event, methods);
+    };
+
+  } else {
+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
+    Object.extend(Event.prototype, methods);
+    return Prototype.K;
+  }
+})();
+
+Object.extend(Event, (function() {
+  var cache = Event.cache;
+
+  function getEventID(element) {
+    // Event ID is stored as the 0th index in a one-item array so that it
+    // won't get copied to a new node when cloneNode is called.
+    if (element === window) return 1;
+    if (element._prototypeEventID) return element._prototypeEventID[0];
+    return element._prototypeEventID = [arguments.callee.id++];
+  }
+  getEventID.id = 2;
+
+  function getDOMEventName(eventName) {
+    if (eventName && eventName.include(':')) return "dataavailable";
+    return eventName;
+  }
+
+  function getCacheForID(id) {
+    return cache[id] = cache[id] || { };
+  }
+
+  function addEventDispatcher(element, eventName, dispatchWrapper) {
+    var id = getEventID(element), wrappers = getWrappersForEventName(id, eventName);
+    if (wrappers.dispatcher) return;
+
+    wrappers.dispatcher = function(event) {
+      var w = getWrappersForEventName(id, eventName);
+      for(var i = 0, l = w.length; i < l; i++) w[i](event); // execute wrappers
+    };
+
+    if (dispatchWrapper) wrappers.dispatcher = wrappers.dispatcher.wrap(dispatchWrapper);
+    element.attachEvent("on" + getDOMEventName(eventName), wrappers.dispatcher);
+  }
+
+  function getWrappersForEventName(id, eventName) {
+    var c = getCacheForID(id);
+    return c[eventName] = c[eventName] || [];
+  }
+
+  function createWrapper(element, eventName, handler) {
+    var id = getEventID(element), c = getCacheForID(id);
+
+    // Attach the element itself onto its cache entry so we can retrieve it for
+    // cleanup on page unload.
+    if (!c.element) c.element = element;
+
+    var w = getWrappersForEventName(id, eventName);
+    if (w.pluck("handler").include(handler)) return false;
+
+    var wrapper = function(event) {
+      if (!Event || !Event.extend ||
+        (event.eventName && event.eventName != eventName))
+          return false;
+
+      handler.call(element, Event.extend(event));
+    };
+
+    wrapper.handler = handler;
+    w.push(wrapper);
+    return wrapper;
+  }
+
+  function findWrapper(id, eventName, handler) {
+    var w = getWrappersForEventName(id, eventName);
+    return w.find(function(wrapper) { return wrapper.handler == handler });
+  }
+
+  function destroyWrapper(id, eventName, handler) {
+    var c = getCacheForID(id);
+    if (!c[eventName]) return false;
+    var d = c[eventName].dispatcher;
+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+    c[eventName].dispatcher = d;
+  }
+
+  // Loop through all elements and remove all handlers on page unload. IE
+  // needs this in order to prevent memory leaks.
+  function purgeListeners() {
+    var element, entry;
+    for (var i in Event.cache) {
+      entry = Event.cache[i];
+      Event.stopObserving(entry.element);
+      entry.element = null;
     }
-    Event.observers = false;
-  },
-
-  observe: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
-
-    if (name == 'keypress' &&
-      (Prototype.Browser.WebKit || element.attachEvent))
-      name = 'keydown';
-
-    Event._observeAndCache(element, name, observer, useCapture);
-  },
-
-  stopObserving: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
-
-    if (name == 'keypress' &&
-        (Prototype.Browser.WebKit || element.attachEvent))
-      name = 'keydown';
-
-    if (element.removeEventListener) {
-      element.removeEventListener(name, observer, useCapture);
-    } else if (element.detachEvent) {
-      try {
-        element.detachEvent('on' + name, observer);
-      } catch (e) {}
+  }
+
+  function onStop() {
+    document.detachEvent("onstop", onStop);
+    purgeListeners();
+  }
+
+  function onBeforeUnload() {
+    if (document.readyState === "interactive") {
+      document.attachEvent("onstop", onStop);
+      (function() { document.detachEvent("onstop", onStop); }).defer();
     }
   }
+
+  if (window.attachEvent && !window.addEventListener) {
+    // Internet Explorer needs to remove event handlers on page unload
+    // in order to avoid memory leaks.
+    window.attachEvent("onunload", purgeListeners);
+
+    // IE also doesn't fire the unload event if the page is navigated away
+    // from before it's done loading. Workaround adapted from
+    // http://blog.moxiecode.com/2008/04/08/unload-event-never-fires-in-ie/.
+    window.attachEvent("onbeforeunload", onBeforeUnload);
+
+    // Ensure window onload is fired after "dom:loaded"
+    addEventDispatcher(window, 'load', function(proceed, event) {
+    	if (document.loaded) {
+    	  proceed(event);
+    	} else {
+    	  arguments.callee.defer(proceed, event);
+    	}
+    });
+
+    // Ensure window onresize is fired only once per resize
+    addEventDispatcher(window, 'resize', function(proceed, event) {
+      var callee = arguments.callee, dimensions = document.viewport.getDimensions();
+      if (dimensions.width != callee.prevWidth || dimensions.height != callee.prevHeight) {
+        callee.prevWidth  = dimensions.width;
+        callee.prevHeight = dimensions.height;
+        proceed(event);
+      }
+    });
+  }
+
+  // Safari has a dummy event handler on page unload so that it won't
+  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
+  // object when page is returned to via the back button using its bfcache.
+  else if (Prototype.Browser.WebKit) {
+    window.addEventListener("unload", Prototype.emptyFunction, false);
+  }
+
+  return {
+    observe: function(element, eventName, handler) {
+      element = $(element);
+      var name = getDOMEventName(eventName);
+
+      var wrapper = createWrapper(element, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.addEventListener) {
+        element.addEventListener(name, wrapper, false);
+      } else {
+        addEventDispatcher(element, eventName);
+      }
+
+      return element;
+    },
+
+    stopObserving: function(element, eventName, handler) {
+      element = $(element);
+      eventName = Object.isString(eventName) ? eventName : null;
+      var id = getEventID(element), c = cache[id];
+
+      if (!c) {
+        return element;
+      }
+      else if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          Event.stopObserving(element, eventName, wrapper.handler);
+        });
+        return element;
+      }
+      else if (!eventName) {
+        Object.keys(c).without("element").each(function(eventName) {
+          Event.stopObserving(element, eventName);
+        });
+        return element;
+      }
+
+      var wrapper = findWrapper(id, eventName, handler);
+      if (!wrapper) return element;
+
+      var name = getDOMEventName(eventName);
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+        destroyWrapper(id, eventName, handler);
+      } else {
+        destroyWrapper(id, eventName, handler);
+        var wrappers = getWrappersForEventName(id, eventName);
+        if (!wrappers.length) {
+          element.detachEvent("on" + name, wrappers.dispatcher);
+          wrappers.dispatcher = null;
+        }
+      }
+
+      return element;
+    },
+
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
+
+      var event;
+      if (document.createEvent) {
+        event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        event = document.createEventObject();
+        event.eventType = "ondataavailable";
+      }
+
+      event.eventName = eventName;
+      event.memo = memo || { };
+
+      if (document.createEvent) {
+        element.dispatchEvent(event);
+      } else {
+        element.fireEvent(event.eventType, event);
+      }
+
+      return Event.extend(event);
+    }
+  };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
 });
 
-/* prevent memory leaks in IE */
-if (Prototype.Browser.IE)
-  Event.observe(window, 'unload', Event.unloadCache, false);
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize(),
+  loaded:        false
+});
+
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards, John Resig and Diego Perini. */
+
+  var timer;
+
+  function fireContentLoadedEvent() {
+    if (document.loaded) return;
+    if (timer) window.clearInterval(timer);
+    document.loaded = true;
+    document.fire("dom:loaded");
+  }
+
+  if (document.addEventListener) {
+    document.addEventListener("DOMContentLoaded", fireContentLoadedEvent, false);
+  } else {
+    document.attachEvent("onreadystatechange", function() {
+      if (document.readyState == "complete") {
+        document.detachEvent("onreadystatechange", arguments.callee);
+        fireContentLoadedEvent();
+      }
+    });
+
+    if (window == top) {
+      timer = setInterval(function() {
+        try {
+          document.documentElement.doScroll("left");
+        } catch(e) { return }
+        fireContentLoadedEvent();
+      }, 10);
+    }
+  }
+
+
+  // Safari <3.1 doesn't support DOMContentLoaded
+  if (Prototype.Browser.WebKit && (navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1] < 525)) {
+    timer = setInterval(function() {
+      if (/loaded|complete/.test(document.readyState))
+        fireContentLoadedEvent();
+    }, 10);
+  }
+
+  // Worst case fallback...
+  Event.observe(window, "load", fireContentLoadedEvent);
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
+  },
+
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
+
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
+
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
+  }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
 var Position = {
   // set to true if needed, warning: firefox performance problems
   // NOT neeeded for page scrolling, only if draggable contained in
   // scrollable elements
   includeScrollOffsets: false,
 
   // must be called before calling withinIncludingScrolloffset, every time the
   // page is scrolled
@@ -3026,214 +4531,151 @@ var Position = {
                 || document.body.scrollLeft
                 || 0;
     this.deltaY =  window.pageYOffset
                 || document.documentElement.scrollTop
                 || document.body.scrollTop
                 || 0;
   },
 
-  realOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.scrollTop  || 0;
-      valueL += element.scrollLeft || 0;
-      element = element.parentNode;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  cumulativeOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  positionedOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-      if (element) {
-        if(element.tagName=='BODY') break;
-        var p = Element.getStyle(element, 'position');
-        if (p == 'relative' || p == 'absolute') break;
-      }
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  offsetParent: function(element) {
-    if (element.offsetParent) return element.offsetParent;
-    if (element == document.body) return element;
-
-    while ((element = element.parentNode) && element != document.body)
-      if (Element.getStyle(element, 'position') != 'static')
-        return element;
-
-    return document.body;
-  },
-
   // caches x/y coordinate pair to use with overlap
   within: function(element, x, y) {
     if (this.includeScrollOffsets)
       return this.withinIncludingScrolloffsets(element, x, y);
+
+    var dimensions = Element.getDimensions(element);
     this.xcomp = x;
     this.ycomp = y;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (y >= this.offset[1] &&
-            y <  this.offset[1] + element.offsetHeight &&
+            y <  this.offset[1] + dimensions.height &&
             x >= this.offset[0] &&
-            x <  this.offset[0] + element.offsetWidth);
+            x <  this.offset[0] + dimensions.width);
   },
 
   withinIncludingScrolloffsets: function(element, x, y) {
-    var offsetcache = this.realOffset(element);
+    var offsetcache = Element.cumulativeScrollOffset(element),
+    dimensions = Element.getDimensions(element);
 
     this.xcomp = x + offsetcache[0] - this.deltaX;
     this.ycomp = y + offsetcache[1] - this.deltaY;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (this.ycomp >= this.offset[1] &&
-            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.ycomp <  this.offset[1] + dimensions.height &&
             this.xcomp >= this.offset[0] &&
-            this.xcomp <  this.offset[0] + element.offsetWidth);
+            this.xcomp <  this.offset[0] + dimensions.width);
   },
 
   // within must be called directly before
   overlap: function(mode, element) {
+    var dimensions = Element.getDimensions(element);
     if (!mode) return 0;
     if (mode == 'vertical')
-      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
-        element.offsetHeight;
+      return ((this.offset[1] + dimensions.height) - this.ycomp) /
+        dimensions.height;
     if (mode == 'horizontal')
-      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
-        element.offsetWidth;
-  },
-
-  page: function(forElement) {
-    var valueT = 0, valueL = 0;
-
-    var element = forElement;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-
-      // Safari fix
-      if (element.offsetParent == document.body)
-        if (Element.getStyle(element,'position')=='absolute') break;
-
-    } while (element = element.offsetParent);
-
-    element = forElement;
-    do {
-      if (!window.opera || element.tagName=='BODY') {
-        valueT -= element.scrollTop  || 0;
-        valueL -= element.scrollLeft || 0;
-      }
-    } while (element = element.parentNode);
-
-    return [valueL, valueT];
-  },
-
-  clone: function(source, target) {
-    var options = Object.extend({
-      setLeft:    true,
-      setTop:     true,
-      setWidth:   true,
-      setHeight:  true,
-      offsetTop:  0,
-      offsetLeft: 0
-    }, arguments[2] || {})
-
-    // find page position of source
-    source = $(source);
-    var p = Position.page(source);
-
-    // find coordinate system to use
-    target = $(target);
-    var delta = [0, 0];
-    var parent = null;
-    // delta [0,0] will do fine with position: fixed elements,
-    // position:absolute needs offsetParent deltas
-    if (Element.getStyle(target,'position') == 'absolute') {
-      parent = Position.offsetParent(target);
-      delta = Position.page(parent);
-    }
-
-    // correct by body offsets (fixes Safari)
-    if (parent == document.body) {
-      delta[0] -= document.body.offsetLeft;
-      delta[1] -= document.body.offsetTop;
-    }
-
-    // set position
-    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
-    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
-    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
-    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
-  },
+      return ((this.offset[0] + dimensions.width) - this.xcomp) /
+        dimensions.width;
+  },
+
+  // Deprecation layer -- use newer Element methods now (1.5.2).
+
+  cumulativeOffset: Element.Methods.cumulativeOffset,
+
+  positionedOffset: Element.Methods.positionedOffset,
 
   absolutize: function(element) {
-    element = $(element);
-    if (element.style.position == 'absolute') return;
     Position.prepare();
-
-    var offsets = Position.positionedOffset(element);
-    var top     = offsets[1];
-    var left    = offsets[0];
-    var width   = element.clientWidth;
-    var height  = element.clientHeight;
-
-    element._originalLeft   = left - parseFloat(element.style.left  || 0);
-    element._originalTop    = top  - parseFloat(element.style.top || 0);
-    element._originalWidth  = element.style.width;
-    element._originalHeight = element.style.height;
-
-    element.style.position = 'absolute';
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.width  = width + 'px';
-    element.style.height = height + 'px';
+    return Element.absolutize(element);
   },
 
   relativize: function(element) {
-    element = $(element);
-    if (element.style.position == 'relative') return;
     Position.prepare();
-
-    element.style.position = 'relative';
-    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
-    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
-
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.height = element._originalHeight;
-    element.style.width  = element._originalWidth;
+    return Element.relativize(element);
+  },
+
+  realOffset: Element.Methods.cumulativeScrollOffset,
+
+  offsetParent: Element.Methods.getOffsetParent,
+
+  page: Element.Methods.viewportOffset,
+
+  clone: function(source, target, options) {
+    options = options || { };
+    return Element.clonePosition(target, source, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+  function iter(name) {
+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
   }
-}
-
-// Safari returns margins on body which is incorrect if the child is absolutely
-// positioned.  For performance reasons, redefine Position.cumulativeOffset for
-// KHTML/WebKit only.
-if (Prototype.Browser.WebKit) {
-  Position.cumulativeOffset = function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      if (element.offsetParent == document.body)
-        if (Element.getStyle(element, 'position') == 'absolute') break;
-
-      element = element.offsetParent;
-    } while (element);
-
-    return [valueL, valueT];
+
+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+  function(element, className) {
+    className = className.toString().strip();
+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+  } : function(element, className) {
+    className = className.toString().strip();
+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+    if (!classNames && !className) return elements;
+
+    var nodes = $(element).getElementsByTagName('*');
+    className = ' ' + className + ' ';
+
+    for (var i = 0, child, cn; child = nodes[i]; i++) {
+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+          (classNames && classNames.all(function(name) {
+            return !name.toString().blank() && cn.include(' ' + name + ' ');
+          }))))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  };
+
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
   }
-}
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
 
 Element.addMethods();
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/dist/prototype_update_helper.js
@@ -0,0 +1,415 @@
+/* Update Helper (c) 2008 Tobie Langel
+ *
+ * Requires Prototype >= 1.6.0
+ *
+ * Update Helper is distributable under the same terms as Prototype
+ * (MIT-style license). For details, see the Prototype web site:
+ * http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var UpdateHelper = Class.create({
+  logLevel: 0,
+  MessageTemplate: new Template('Update Helper: #{message}\n#{stack}'),
+  Regexp:          new RegExp("@" + window.location.protocol + ".*?\\d+\\n", "g"),
+
+  initialize: function(deprecatedMethods) {
+    var notify = function(message, type) {
+      this.notify(message, type);
+    }.bind(this);   // Late binding to simplify testing.
+
+    deprecatedMethods.each(function(d) {
+      var condition = d.condition,
+          type      = d.type || 'info',
+          message   = d.message,
+          namespace = d.namespace,
+          method    = d.methodName;
+
+      namespace[method] = (namespace[method] || function() {}).wrap(function(proceed) {
+        var args = $A(arguments).splice(1);
+        if (!condition || condition.apply(this, args)) notify(message, type);
+        return proceed.apply(proceed, args);
+      });
+    });
+    Element.addMethods();
+  },
+
+  notify: function(message, type) {
+    switch(type) {
+      case 'info':
+        if (this.logLevel > UpdateHelper.Info) return false;
+      case 'warn':
+        if (this.logLevel > UpdateHelper.Warn) return false;
+      default:
+        if (this.logLevel > UpdateHelper.Error) return false;
+    }
+    this.log(this.MessageTemplate.evaluate({
+      message: message,
+      stack: this.getStack()
+    }), type);
+    return true;
+  },
+
+  getStack: function() {
+    try {
+      throw new Error("stack");
+    } catch(e) {
+      return (e.stack || '').match(this.Regexp).reject(function(path) {
+        return /(prototype|unittest|update_helper)\.js/.test(path);
+      }).join("\n");
+    }
+  },
+
+  log: function(message, type) {
+    if (type == 'error') console.error(message);
+    else if (type == 'warn') console.warn(message);
+    else console.log(message);
+  }
+});
+
+Object.extend(UpdateHelper, {
+  Info:  0,
+  Warn:  1,
+  Error: 2
+});
+
+/* UpdateHelper for Prototype 1.6.0.2 (c) 2008 Tobie Langel
+ *
+ * UpdateHelper for Prototype is freely distributable under the same
+ * terms as Prototype (MIT-style license).
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ * Include this file right below prototype.js. All messages
+ * will be logged to the console.
+ *
+ * Note: You can tune the level of warning by redefining
+ * prototypeUpdateHelper.logLevel with one of the appropriate constansts
+ * (UpdateHelper.Info, UpdateHelper.Warn or UpdateHelper.Error), e.g.:
+ *
+ *     prototypeUpdateHelper.logLevel = UpdateHelper.Warn;
+ *
+ * This, for example, will prevent deprecation messages from being logged.
+ *
+ *                THIS SCRIPT WORKS IN FIREFOX ONLY
+ *--------------------------------------------------------------------------*/
+
+var prototypeUpdateHelper = new UpdateHelper([
+  {
+    methodName: 'display',
+    namespace: Toggle,
+    message: 'Toggle.display has been deprecated, please use Element.toggle instead.'
+  },
+
+  {
+    methodName: 'show',
+    namespace: Element.Methods,
+    message: 'Passing an arbitrary number of elements to Element.show is no longer supported.\n' +
+      'Use [id_1, id_2, ...].each(Element.show) or $(id_1, id_2, ...).invoke("show") instead.',
+    type: 'error',
+    condition: function() { return arguments.length > 1 && !Object.isNumber(arguments[1]) }
+  },
+
+  {
+    methodName: 'hide',
+    namespace: Element.Methods,
+    message: 'Passing an arbitrary number of elements to Element.hide is no longer supported.\n' +
+      'Use [id_1, id_2, ...].each(Element.hide) or $(id_1, id_2, ...).invoke("hide") instead.',
+    type: 'error',
+    condition: function() { return arguments.length > 1 && !Object.isNumber(arguments[1]) }
+  },
+
+  {
+    methodName: 'toggle',
+    namespace: Element.Methods,
+    message: 'Passing an arbitrary number of elements to Element.toggle is no longer supported.\n' +
+      'Use [id_1, id_2, ...].each(Element.toggle) or $(id_1, id_2, ...).invoke("toggle") instead.',
+    type: 'error',
+    condition: function() { return arguments.length > 1 && !Object.isNumber(arguments[1]) }
+  },
+
+  {
+    methodName: 'clear',
+    namespace: Form.Element.Methods,
+    message: 'Passing an arbitrary number of elements to Field.clear is no longer supported.\n' +
+      'Use [id_1, id_2, ...].each(Form.Element.clear) or $(id_1, id_2, ...).invoke("clear") instead.',
+    type: 'error',
+    condition: function() { return arguments.length > 1 && !Object.isNumber(arguments[1]) }
+  },
+
+  {
+    methodName: 'present',
+    namespace: Form.Element.Methods,
+    message: 'Passing an arbitrary number of elements to Field.present is no longer supported.\n' +
+      'Use [id_1, id_2, ...].each(Form.Element.present) or $(id_1, id_2, ...).invoke("present") instead.',
+    type: 'error',
+    condition: function() { return arguments.length > 1 && !Object.isNumber(arguments[1]) }
+  },
+
+  {
+    methodName: 'childOf',
+    namespace: Element.Methods,
+    message: 'Element#childOf has been deprecated, please use Element#descendantOf instead.'
+  },
+
+  {
+    methodName: 'Before',
+    namespace: Insertion,
+    message: 'Insertion.Before has been deprecated, please use Element#insert instead.'
+  },
+
+  {
+    methodName: 'Top',
+    namespace: Insertion,
+    message: 'Insertion.Top has been deprecated, please use Element#insert instead.'
+  },
+
+  {
+    methodName: 'Bottom',
+    namespace: Insertion,
+    message: 'Insertion.Bottom has been deprecated, please use Element#insert instead.'
+  },
+
+  {
+    methodName: 'After',
+    namespace: Insertion,
+    message: 'Insertion.After has been deprecated, please use Element#insert instead.'
+  },
+
+  {
+    methodName: 'prepare',
+    namespace: Position,
+    message: 'Position.prepare has been deprecated.'
+  },
+
+  {
+    methodName: 'within',
+    namespace: Position,
+    message: 'Position.within has been deprecated.'
+  },
+
+  {
+    methodName: 'withinIncludingScrolloffsets',
+    namespace: Position,
+    message: 'Position.withinIncludingScrolloffsets has been deprecated.'
+  },
+
+  {
+    methodName: 'overlap',
+    namespace: Position,
+    message: 'Position.overlap has been deprecated.'
+  },
+
+  {
+    methodName: 'cumulativeOffset',
+    namespace: Position,
+    message: 'Position.cumulativeOffset has been deprecated, please use Element#cumulativeOffset instead.'
+  },
+
+  {
+    methodName: 'positionedOffset',
+    namespace: Position,
+    message: 'Position.positionedOffset has been deprecated, please use Element#positionedOffset instead.'
+  },
+
+  {
+    methodName: 'absolutize',
+    namespace: Position,
+    message: 'Position.absolutize has been deprecated, please use Element#absolutize instead.'
+  },
+
+  {
+    methodName: 'relativize',
+    namespace: Position,
+    message: 'Position.relativize has been deprecated, please use Element#relativize instead.'
+  },
+
+  {
+    methodName: 'realOffset',
+    namespace: Position,
+    message: 'Position.realOffset has been deprecated, please use Element#cumulativeScrollOffset instead.'
+  },
+
+  {
+    methodName: 'offsetParent',
+    namespace: Position,
+    message: 'Position.offsetParent has been deprecated, please use Element#getOffsetParent instead.'
+  },
+
+  {
+    methodName: 'page',
+    namespace: Position,
+    message: 'Position.page has been deprecated, please use Element#viewportOffset instead.'
+  },
+
+  {
+    methodName: 'clone',
+    namespace: Position,
+    message: 'Position.clone has been deprecated, please use Element#clonePosition instead.'
+  },
+
+  {
+    methodName: 'initialize',
+    namespace: Element.ClassNames.prototype,
+    message: 'Element.ClassNames has been deprecated.'
+  },
+
+  {
+    methodName: 'classNames',
+    namespace: Element.Methods,
+    message: 'Element#classNames has been deprecated.\n' +
+      'If you need to access CSS class names as an array, try: $w(element.classname).'
+  },
+
+  {
+    methodName: 'setStyle',
+    namespace: Element.Methods,
+    message: 'Use of uncamelized style-property names is no longer supported.\n' +
+      'Use either camelized style-property names or a regular CSS string instead (see online documentation).',
+    type: 'error',
+    condition: function(element, style) {
+      return !Object.isString(style) && Object.keys(style).join('').include('-');
+    }
+  },
+
+  {
+    methodName: 'getElementsByClassName',
+    namespace: document,
+    message: 'document.getElementsByClassName has been deprecated, please use $$ instead.'
+  },
+
+  {
+    methodName: 'getElementsByClassName',
+    namespace: Element.Methods,
+    message: 'Element#getElementsByClassName has been deprecated, please use Element#select instead.'
+  },
+
+  {
+    methodName: 'immediateDescendants',
+    namespace: Element.Methods,
+    message: 'Element#immediateDescendants has been deprecated, please use Element#childElements instead.'
+  },
+
+  {
+    methodName: 'getElementsBySelector',
+    namespace: Element.Methods,
+    message: 'Element#getElementsBySelector has been deprecated, please use Element#select instead.'
+  },
+
+  {
+    methodName: 'toQueryString',
+    namespace: Hash,
+    message: 'Hash.toQueryString has been deprecated.\n' +
+      'Use the instance method Hash#toQueryString or Object.toQueryString instead.'
+  },
+
+  {
+    methodName: 'toJSON',
+    namespace: Hash,
+    message: 'Hash.toJSON has been removed.\n' +
+      'Use the instance method Hash#toJSON or Object.toJSON instead.',
+    type: 'error'
+  },
+
+  {
+    methodName: 'remove',
+    namespace: Hash.prototype,
+    message: 'Hash#remove is no longer supported, use Hash#unset instead.\n' +
+      'Please note that Hash#unset only accepts one argument.',
+    type: 'error'
+  },
+
+  {
+    methodName: 'merge',
+    namespace: Hash.prototype,
+    message: 'Hash#merge is no longer destructive and now operates on a clone of the Hash instance.\n' +
+      'If you need a destructive merge, use Hash#update instead.',
+    type: 'warn'
+  },
+
+  {
+    methodName: 'unloadCache',
+    namespace: Event,
+    message: 'Event.unloadCache has been deprecated.',
+    type: 'error'
+  },
+
+  {
+    methodName: 'create',
+    namespace: Class,
+    message: 'The class API has been fully revised and now allows for mixins and inheritance.\n' +
+      'You can find more about it here: http://prototypejs.org/learn/class-inheritance',
+    condition: function() { return !arguments.length }
+  }
+]);
+
+// Special casing for Hash.
+
+(function() {
+  var __properties = Object.keys(Hash.prototype).concat(['_object', '__properties']);
+
+  var messages = {
+    setting: new Template("Directly setting a property on an instance of Hash is no longer supported.\n" +
+    "Please use Hash#set('#{property}', #{value}) instead."),
+    getting: new Template("Directly accessing a property of an instance of Hash is no longer supported.\n" +
+      "Please use Hash#get('#{property}') instead.")
+  };
+
+  function notify(property, value) {
+    var message = messages[arguments.length == 1 ? 'getting' : 'setting'].evaluate({
+      property: property,
+      value: Object.inspect(value)
+    });
+    prototypeUpdateHelper.notify(message, 'error');
+  }
+
+  function defineSetters(obj, prop) {
+    if (obj.__properties.include(prop)) return;
+    obj.__properties.push(prop);
+    obj.__defineGetter__(prop, function() {
+      checkProperties(this);
+      notify(prop);
+    });
+    obj.__defineSetter__(prop, function(value) {
+      checkProperties(this);
+      notify(prop, value);
+    });
+  }
+
+  function checkProperties(hash) {
+    var current = Object.keys(hash);
+    if (current.length == hash.__properties.length)
+      return;
+    current.each(function(prop) {
+      if (hash.__properties.include(prop)) return;
+      notify(prop, hash[prop]);
+      defineSetters(hash, prop);
+    });
+  }
+
+  Hash.prototype.set = Hash.prototype.set.wrap(function(proceed, property, value) {
+    defineSetters(this, property);
+    return proceed(property, value);
+  });
+
+  $w('merge update').each(function(name) {
+    Hash.prototype[name] = Hash.prototype[name].wrap(function(proceed, object) {
+      for (var prop in object) defineSetters(this, prop);
+      return proceed(object);
+    });
+  });
+
+  $H(Hash.prototype).each(function(method) {
+    var key = method.key;
+    if (!Object.isFunction(method.value) || key == 'initialize') return;
+    Hash.prototype[key] = Hash.prototype[key].wrap(function(proceed) {
+      checkProperties(this);
+      return proceed.apply(proceed, $A(arguments).splice(1));
+    });
+  });
+
+  Hash.prototype.initialize = Hash.prototype.initialize.wrap(function(proceed, object) {
+    this.__properties = __properties.clone();
+    for (var prop in object) defineSetters(this, prop);
+    proceed(object);
+  });
+})();
--- a/dom/tests/mochitest/ajax/prototype/manifest.json
+++ b/dom/tests/mochitest/ajax/prototype/manifest.json
@@ -1,1 +1,1 @@
-{testcases:["test/unit/ajax.html","test/unit/array.html","test/unit/base.html","test/unit/dom.html","test/unit/element_mixins.html","test/unit/enumerable.html","test/unit/form.html","test/unit/hash.html","test/unit/position.html","test/unit/range.html","test/unit/selector.html","test/unit/string.html","test/unit/unit_tests.html"]}
\ No newline at end of file
+{testcases:["test/unit/tmp/ajax_test.html","test/unit/tmp/array_test.html","test/unit/tmp/base_test.html","test/unit/tmp/dom_test.html","test/unit/tmp/element_mixins_test.html","test/unit/tmp/enumerable_test.html","test/unit/tmp/event_test.html","test/unit/tmp/form_test.html","test/unit/tmp/hash_test.html","test/unit/tmp/number_test.html","test/unit/tmp/position_test.html","test/unit/tmp/range_test.html","test/unit/tmp/selector_test.html","test/unit/tmp/string_test.html", "test/unit/tmp/unit_test.html"]}
\ No newline at end of file
--- a/dom/tests/mochitest/ajax/prototype/test/Makefile.in
+++ b/dom/tests/mochitest/ajax/prototype/test/Makefile.in
@@ -11,20 +11,21 @@
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is mozilla.org code.
 #
 # The Initial Developer of the Original Code is
 # Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2007
+# Portions created by the Initial Developer are Copyright (C) 2008
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
 # or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -40,19 +41,22 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= dom/tests/mochitest/ajax/prototype/test
 
 include $(DEPTH)/config/autoconf.mk
 
 DIRS	= \
 	unit \
-        lib \
+	lib \
+	functional \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
 		test.css \
+		browser.html \
+		console.html \
 		$(NULL)
 
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/browser.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    <title>Prototype object browser</title>
+    <style type="text/css" media="screen">
+      body {
+        font-family: Lucida Grande, Verdana, sans-serif;
+        font-size: 13px;
+      }
+      
+      .inspector {
+        margin: 1%;
+        float: left;
+        width: 31%;
+        border: 1px solid #ccc;
+        background-color: white;
+      }
+      
+      .inspector h2 {
+        font-size: 13px;
+        margin: 0;
+        text-align: center;
+        padding: 5px;
+        background-color: #e6e6e6;
+        border-bottom: 1px solid #999;
+      }
+      
+      .inspector ul {
+        height: 200px;
+        overflow: auto;
+        margin: 0;
+        padding-left: 0;
+      }
+      
+      .inspector li {
+        cursor: pointer;
+        list-style-type: none;
+        padding: 2px 5px 2px 30px;
+        color: #333;
+      }
+      
+      .inspector li.selected {
+        background-color: #888;
+        color: #fff;
+      }
+      
+      .inspector.active li.selected {
+        background-color: #1a76fd;
+        color: #fff;
+      }
+      
+      #path, #value {
+        width: 97%;
+        margin: 1%;
+      }
+
+      #path {
+        margin-bottom: 0;
+        border: 1px solid #ccc;
+        border-bottom: 1px solid #999;
+        background-color: #e6e6e6;
+      }
+      
+      #value {
+        margin-top: 0;
+        border: 1px solid #ccc;
+        border-top: none;
+        overflow: auto;
+      }
+      
+      #path_content, #value_content {
+        display: block;
+        padding: 15px 30px 15px 30px;
+      }
+      
+    </style>    
+    <script type="text/javascript" src="../dist/prototype.js"></script>
+    <script type="text/javascript">
+      var Browser = Class.create();
+      Browser.prototype = {
+        initialize: function(element, name, value, options) {
+          this.element = $(element);
+          this.name    = name;
+          this.value   = value;
+          this.history = [];
+          Object.extend(this, options || {});
+          this.reset();
+        },
+        
+        reset: function() {
+          this.go(this.name, this.value);
+        },
+        
+        refresh: function() {
+          var children = $A(this.element.childNodes),
+              history  = this.history.toArray(),
+              elements = history.slice(-3).pluck('element');
+                  
+          children.each(function(element) {
+            if (element && !elements.include(element))
+              this.element.removeChild(element);
+          }.bind(this));
+          
+          children = $A(this.element.childNodes);
+          
+          elements.each(function(element, index) {
+            Element.removeClassName(element, 'active');
+            var child = children[index];
+            if (!child)
+              this.element.appendChild(element);
+            else if (!element.parentNode)
+              this.element.insertBefore(element, child);
+          }.bind(this));
+          
+          this.setTitle();
+          this.setValue();
+        },
+        
+        setTitle: function() {
+          if (this.titleElement)
+            this.titleElement.innerHTML =
+              this.history.pluck('name').invoke('escapeHTML').join('.');
+        },
+        
+        setValue: function() {
+          if (this.valueElement)
+            this.valueElement.innerHTML = 
+              this.currentValue().escapeHTML() + '&nbsp;';
+        },
+        
+        currentValue: function() {
+          try {
+            return Object.inspect(this.current());
+          } catch (e) {
+            return '(Internal Function)';
+          }
+        },
+        
+        current: function() {
+          return this.history.last().value;
+        },
+        
+        go: function(name, value) {
+          var from = this.history.last();
+          this.history.push(new Inspector(this, name, value));
+          this.refresh();
+          if (from) 
+            Element.addClassName(from.element, 'active');
+        }
+      }
+      
+      var Inspector = Class.create();
+      Inspector.prototype = {
+        initialize: function(browser, name, value) {
+          this.browser = browser;
+          this.name    = name;
+          this.value   = value;
+          this.id      = 'inspector_' + new Date().getTime();
+          this.history = this.browser.history.toArray();
+          this.history.push(this);
+          this.createElement();
+          this.populate();
+        },
+        
+        properties: function() {
+          var properties = [];
+          for (var property in this.value)
+            properties.push(property);
+          properties.sort();
+          return properties;
+        },
+        
+        createElement: function() {
+          var element = document.createElement('div');
+          element.className = 'inspector';
+          element.id = this.id;
+          this.element = element;
+          
+          var title = document.createElement('h2');
+          title.innerHTML = this.name.toString().escapeHTML();
+          this.titleElement = title;
+          
+          var list = document.createElement('ul');
+          this.listElement = list;
+          
+          element.appendChild(title);
+          element.appendChild(list);          
+        },
+        
+        populate: function() {
+          this.properties().each(function(property) {
+            var li = document.createElement('li');
+            li.innerHTML = property.toString().escapeHTML();
+            li.onclick   = this.select.bind(this, li);
+            li._property = property;
+            this.listElement.appendChild(li);
+          }.bind(this));
+        },
+        
+        select: function(element) {
+          this.unselect();
+          Element.addClassName(element, 'selected');
+          this.selectedProperty = element;
+          this.browser.history = this.history.toArray();
+          this.browser.go(element._property, this.value[element._property]);
+        },
+        
+        unselect: function() {
+          if (this.selectedProperty)
+            Element.removeClassName(this.selectedProperty, 'selected');
+          this.selectedProperty = null;
+        }
+      }
+    </script>
+  </head>
+  <body>
+    <div id="browser_wrapper">
+      <div id="browser"></div>
+      <div style="clear: left"></div>
+    </div>
+    <h1 id="path"><span id="path_content"></span></h1>
+    <pre id="value"><div id="value_content"></div></pre>
+    <script type="text/javascript">
+      new Browser('browser', 'window', window, {titleElement: $('path_content'), valueElement: $('value_content')})
+    </script>
+  </body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/console.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+        "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> 
+  <head>
+    <title>Prototype Console</title>
+    <script src="../dist/prototype.js" type="text/javascript"></script>
+    <script type="text/javascript">
+      Prototype.Console = Class.create();
+      Prototype.Console.prototype = {
+        initialize: function(element, form, input) {
+          this.element = $(element);
+          this.form    = $(form);
+          this.input   = $(input);
+          this.context = window.eval.bind(window);
+          this.registerCallbacks();
+          document.title = 'Prototype Console ' + Prototype.Version;
+          Field.activate(this.input);
+        },
+        
+        registerCallbacks: function() {
+          Event.observe(this.form, 'submit', function(event) {
+            this.eval($F(this.input));
+            this.input.value = '';
+            Field.activate(this.input);
+            Event.stop(event);
+          }.bind(this));
+        },
+        
+        log: function(type, message) {
+          new Insertion.Bottom(this.element, 
+            '<tr class="' + type + '"><td>' +
+            message.escapeHTML() + '</td></tr>');
+          Element.scrollTo(this.form);
+        },
+        
+        eval: function(expression) {
+          if (expression.match(/^\s*$/)) return;
+          try {
+            this.log('input', expression);
+            window.$_ = this.context.call(window, expression);
+            this.log('output', Object.inspect($_));
+          } catch (e) {
+            this.log('error', e.toString());
+          }
+        },
+        
+        clear: function() {
+          this.element.innerHTML = '';
+        }
+      }
+    </script>
+    <style type="text/css">
+      body {
+        margin: 0;
+        padding: 0;
+      }
+      
+      .console {
+        width: 100%;
+        border-collapse: collapse;
+        margin-bottom: 50px;
+      }
+    
+      .console td {
+        padding: 5px;
+        font-family: monospace;
+        font-size: 14px;
+      }
+      
+      .console tr.input td {
+        background-color: #eee;
+        font-weight: bold;
+      }
+      
+      .console tr.error td,
+      .console tr.output td {
+        color: #333;
+        border-bottom: 1px solid #ccc;
+      }
+      
+      .console tr.error td {
+        color: #f00;
+      }
+      
+      #input-form {
+        width: 100%;
+        background-color: #f0f5b8;
+        border-top: 1px solid #333;
+        padding: 10px;
+        position: fixed;
+        height: 25px;
+        bottom: 0;
+        margin: 0;
+      }
+    </style>
+  </head>
+  <body>
+    <table class="console">
+      <tbody id="console">
+      </tbody>
+    </table>
+    <form id="input-form">
+      <input type="text" size="60" id="input" />
+      <input type="submit" value="Evaluate" />
+    </form>
+    <script type="text/javascript">
+      window.console = new Prototype.Console('console', 'input-form', 'input');
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/functional/Makefile.in
@@ -0,0 +1,53 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (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.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir	= dom/tests/mochitest/ajax/prototype/test/functional
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES	= \
+	event.html \
+	$(NULL)
+
+libs::	$(_TEST_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/functional/event.html
@@ -0,0 +1,243 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+  <title>Prototype functional test file</title>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <script src="../../dist/prototype.js"       type="text/javascript"></script>
+  
+  <style type="text/css" media="screen">
+  /* <![CDATA[ */
+    body { margin:1em 2em; padding:0; font-size:0.8em }
+    hr { width:31.2em; margin:1em 0; text-align:left }
+    p { width:30em; margin:0.5em 0; padding:0.3em 0.6em; color:#222; background:#eee; border:1px solid silver; }
+    .subtest { margin-top:-0.5em }
+    .passed { color:green; border-color:olive }
+    .failed { color:firebrick; border-color:firebrick }
+    .button { padding:0.2em 0.4em; background:#ccc; border:1px solid #aaa }
+    #log { position:absolute; left:35em; top:5em; width:20em; font-size:13px !important }
+    h2 { font:normal 1.1em Verdana,Arial,sans-serif; font-style:italic; color:gray; margin-top:-1.2em }
+    h2 *, h2 a:visited { color:#444 }
+    h2 a:hover { color:blue }
+    a:visited { color:blue }
+    a:hover { color:red }
+  /* ]]> */
+  </style>
+  
+  <script type="text/javascript">
+    Element.addMethods({
+      passed: function(el, message) {
+        el = $(el);
+        el.className = 'passed';
+        (el.down('span') || el).update(message || 'Test passed!');
+      },
+      
+      failed: function(el, message) {
+        el = $(el);
+        el.className = 'failed';
+        (el.down('span') || el).update(message || 'Test failed');
+      }
+    });
+    
+    function log(obj) {
+      var line, all = [];
+      for (prop in obj) {
+        if (typeof obj[prop] == 'function' || /^[A-Z]|[XY]$/.test(prop)) continue;
+        line = prop + ": " + Object.inspect(obj[prop]);
+        all.push(line.escapeHTML());
+      }
+      $('log').update(all.join('<br />'));
+    }
+  </script>
+</head>
+<body>
+  <h1>Prototype functional tests for the Event module</h1>
+  
+  <div id="log">log empty</div>
+  
+  <p id="basic">A basic event test - <strong>click here</strong></p>
+  <p id="basic_remove" class="subtest"><strong>click</strong> to stop observing the first test</p>
+  
+  <p id="inline_test" onclick="Event.stop(event); $(this).passed();"><strong>click</strong> to ensure generic Event methods work on inline handlers</p>
+  
+  <script type="text/javascript">
+    var basic_callback = function(e){
+      $('basic').passed();
+      if ($('basic_remove')) $('basic_remove').show()
+      else $('basic').failed()
+      log(e);
+    }
+    $('basic').observe('click', basic_callback)
+    $('basic_remove').observe('click', function(e){
+      el = $('basic')
+      el.passed('This test should now be inactive (try clicking)')
+      el.stopObserving('click')
+      $('basic_remove').remove()
+      log(e);
+    }).hide()
+  </script>
+  
+  <p id="basic2"><strong>Scope</strong> test - scope of the handler should be this element</p>
+  
+  <script type="text/javascript">
+    $('basic2').observe('click', function(e) {
+      if (this === window) $('basic2').failed('Window scope! (needs scope correction)');
+      else this.passed();
+      log(e);
+    });
+  </script>
+
+  <p id="basic3"><strong>Event object</strong> test - should be present as a first argument</p>
+  
+  <script type="text/javascript">
+    $('basic3').observe('click', function(evt) {
+      el = $('basic3');
+      if (typeof evt != 'object') this.failed('Expected event object for first argument');
+      else this.passed('Good first argument');
+      log(evt);
+    });
+  </script>
+
+  <p><a href="#wrong" id="hijack">Hijack link test</a> (preventDefault)</p>
+  
+  <script type="text/javascript">
+    $('hijack').observe('click', function(e){
+      el = $(this.parentNode);
+      log(e); // this makes it fail?!?
+      e.preventDefault();
+
+      setTimeout(function() {
+        if (window.location.hash == '#wrong') el.failed('Hijack failed (<a href="' +
+            window.location.toString().replace(/#.+$/, '') + '">remove the fragment</a>)')
+        else el.passed();
+      }, 50)
+    })
+  </script>
+  
+  <hr />
+
+
+  <p>Mouse click:
+  <span class="button" id="left">left</span> <span class="button" id="middle">middle</span> <span class="button" id="right">right</span></p>
+  
+  <script type="text/javascript">
+    $w('left middle right').each(function(button) {
+      Event.observe(button, 'mousedown', function(e) {
+	  if (Event['is' + this.id.capitalize() + 'Click'](e)) this.passed('Squeak!')
+	  else this.failed('OH NO!');
+	  log(e);
+      });
+    });
+  </script>
+  
+  <p id="context">Context menu event (tries to prevent default)</p>
+  
+  <script type="text/javascript">
+    $('context').observe('contextmenu', function(e){
+      this.passed();
+      Event.stop(e);
+      log(e);
+    })
+  </script>
+
+  <p id="target">Event.element() test</p>
+  
+  <script type="text/javascript">
+    $('target').observe('click', function(e) {
+      if (e.element() == this && e.target == this) this.passed();
+      else this.failed();
+      log(e);
+    });
+  </script>
+
+  <p id="currentTarget"><span>Event.currentTarget test</span></p>
+  
+  <script type="text/javascript">
+    $('currentTarget').observe('click', function(e){
+      if (e.currentTarget !== this) this.failed();
+      else this.passed();
+      log(e);
+    })
+  </script>
+  
+  <p id="findElement"><span>Event.findElement() test</span></p>
+  
+  <script type="text/javascript">
+    $('findElement').observe('click', function(e){
+      if (e.findElement('p') == this && e.findElement('body') == document.body &&
+         e.findElement('foo') == null) this.passed();
+      else this.failed();
+      log(e);
+    })
+  </script>
+  
+  <div id="container"><p id="stop"><strong>Stop propagation</strong> test (bubbling)</p></div>
+  
+  <script type="text/javascript">
+    $('stop').observe('click', function(e){
+      e.stop();
+      this.passed();
+      log(e);
+    })
+    $('container').observe('click', function(e){
+      $('stop').failed();
+      log(e);
+    })
+  </script>
+  
+  <div>
+    <p id="keyup_log"><strong>Keyup</strong> test - focus on the textarea and type</p>
+    <textarea id="keyup" class="subtest"></textarea>
+  </div>
+  
+  <script type="text/javascript">
+    $('keyup').observe('keyup', function(e){
+      el = $('keyup_log');
+      el.passed('Key captured: the length is ' + $('keyup').value.length);
+      log(e);
+    })
+  </script>
+  
+  <p id="bind"><code>bindAsEventListener()</code> test</p>
+  
+  <script type="text/javascript">
+    $('bind').observe('click', function(e, str, arr){
+      el = $('bind')
+      try {
+        if (arguments.length != 3) throw arguments.length + ' arguments: ' + $A(arguments).inspect()
+        if (str != 'foo') throw 'wrong string: ' + str
+        if (arr.constructor != Array) throw '3rd parameter is not an array'
+        el.passed();
+      }
+      catch (err) { el.failed(err.toString()) }
+      log(e);
+    }.bindAsEventListener(document.body, 'foo', [1,2,3]))
+  </script>
+  
+  <p id="obj_inspect"><code>Object.inspect(event)</code> test</p>
+  
+  <script type="text/javascript">
+    $('obj_inspect').observe('click', function(e){
+      el = $('obj_inspect')
+      try { el.passed(Object.inspect(e)) }
+      catch (err) { el.failed('Failed! Error thrown') }
+      log(e);
+    })
+  </script>
+  
+  <p id="addunload">Add unload events</p>
+  
+  <script type="text/javascript">
+    $('addunload').observe('click', function(e){
+      if (this._done) return
+
+      window.onunload = function(){ alert('inline unload fired!') }
+      Event.observe(window, 'unload', function(){ alert('observed unload fired!') })
+
+      this.update('Registered two unload events, one inline ("onunload") and one regular - try to refresh, both should fire')
+      this._done = true
+      log(e);
+    })
+  </script>
+</body>
+</html>
--- a/dom/tests/mochitest/ajax/prototype/test/lib/Makefile.in
+++ b/dom/tests/mochitest/ajax/prototype/test/lib/Makefile.in
@@ -11,20 +11,21 @@
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is mozilla.org code.
 #
 # The Initial Developer of the Original Code is
 # Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2007
+# Portions created by the Initial Developer are Copyright (C) 2008
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
 # or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -37,16 +38,17 @@
 
 DEPTH		= ../../../../../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= dom/tests/mochitest/ajax/prototype/test/lib
 
 include $(DEPTH)/config/autoconf.mk
+
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
 		unittest.js \
 		$(NULL)
 
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
--- a/dom/tests/mochitest/ajax/prototype/test/lib/unittest.js
+++ b/dom/tests/mochitest/ajax/prototype/test/lib/unittest.js
@@ -29,29 +29,27 @@ Event.simulateMouse = function(element, 
     pointerY: 0,
     buttons: 0
   }, arguments[2] || {});
   var oEvent = document.createEvent("MouseEvents");
   oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
     options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
     false, false, false, false, 0, $(element));
   
-  if(this.mark) Element.remove(this.mark);
-  this.mark = document.createElement('div');
+  if (this.mark) Element.remove(this.mark);
+  
+  var style = 'position: absolute; width: 5px; height: 5px;' + 
+    'top: #{pointerY}px; left: #{pointerX}px;'.interpolate(options) + 
+    'border-top: 1px solid red; border-left: 1px solid red;'
+    
+  this.mark = new Element('div', { style: style });
   this.mark.appendChild(document.createTextNode(" "));
   document.body.appendChild(this.mark);
-  this.mark.style.position = 'absolute';
-  this.mark.style.top = options.pointerY + "px";
-  this.mark.style.left = options.pointerX + "px";
-  this.mark.style.width = "5px";
-  this.mark.style.height = "5px;";
-  this.mark.style.borderTop = "1px solid red;"
-  this.mark.style.borderLeft = "1px solid red;"
   
-  if(this.step)
+  if (this.step)
     alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
   
   $(element).dispatchEvent(oEvent);
 };
 
 // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
 // You need to downgrade to 1.0.4 for now to get this working
 // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
@@ -68,494 +66,513 @@ Event.simulateKey = function(element, ev
   var oEvent = document.createEvent("KeyEvents");
   oEvent.initKeyEvent(eventName, true, true, window, 
     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
     options.keyCode, options.charCode );
   $(element).dispatchEvent(oEvent);
 };
 
 Event.simulateKeys = function(element, command) {
-  for(var i=0; i<command.length; i++) {
+  for (var i=0; i<command.length; i++) {
     Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
   }
 };
 
-var Test = {}
-Test.Unit = {};
-
-// security exception workaround
-Test.Unit.inspect = Object.inspect;
+var Test = {
+  Unit: {
+    inspect: Object.inspect // security exception workaround
+  }
+};
 
-Test.Unit.Logger = Class.create();
-Test.Unit.Logger.prototype = {
-  initialize: function(log) {
-    this.log = $(log);
-    if (this.log) {
-      this._createLogTable();
+Test.Unit.Logger = Class.create({
+  initialize: function(element) {
+    this.element = $(element);
+    if (this.element) this._createLogTable();
+    this.tbody = $(this.element.getElementsByTagName('tbody')[0]);
+  },
+  
+  start: function(testName) {
+    this.testName = testName;
+    if (!this.element) return;
+    this.tbody.insert('<tr><td>' + testName + '</td><td></td><td></td></tr>');
+  },
+  
+  setStatus: function(status) {
+    this.getLastLogLine().addClassName(status);
+    $(this.getLastLogLine().getElementsByTagName('td')[1]).update(status);
+  },
+  
+  finish: function(status, summary) {
+    if (!this.element) return;
+    this.setStatus(status);
+    this.message(summary);
+  },
+  
+  message: function(message) {
+    if (!this.element) return;
+    this.getMessageCell().update(this._toHTML(message));
+  },
+  
+  summary: function(summary) {
+    if (!this.element) return;
+    var div = $(this.element.getElementsByTagName('div')[0]);
+    div.update(this._toHTML(summary));
+  },
+  
+  getLastLogLine: function() {
+    //return this.element.descendants('tr').last();
+    var trs = this.element.getElementsByTagName('tr');
+    return $(trs[trs.length - 1]);
+  },
+  
+  getMessageCell: function() {
+    return this.getLastLogLine().down('td', 2);
+    var tds = this.getLastLogLine().getElementsByTagName('td');
+    return $(tds[2]);
+  },
+  
+  _createLogTable: function() {
+    var html = '<div class="logsummary">running...</div>' +
+    '<table class="logtable">' +
+    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+    '<tbody class="loglines"></tbody>' +
+    '</table>';
+    this.element.update(html);
+    
+  },
+  
+  appendActionButtons: function(actions) {
+    actions = $H(actions);
+    if (!actions.any()) return;
+    var div = new Element("div", {className: 'action_buttons'});
+    actions.inject(div, function(container, action) {
+      var button = new Element("input").setValue(action.key).observe("click", action.value);
+      button.type = "button";
+      return container.insert(button);
+    });
+    this.getMessageCell().insert(div);
+  },
+  
+  _toHTML: function(txt) {
+    return txt.escapeHTML().replace(/\n/g,"<br />");
+  }
+});
+
+Test.Unit.Runner = Class.create({
+  initialize: function(testcases) {
+    var options = this.options = Object.extend({
+      testLog: 'testlog'
+    }, arguments[1] || {});
+    
+    options.resultsURL = this.queryParams.resultsURL;
+    
+    this.tests = this.getTests(testcases);
+    this.currentTest = 0;
+
+    Event.observe(window, "load", function() {
+      this.logger = new Test.Unit.Logger($(options.testLog));
+      this.runTests.bind(this).delay(0.1);
+    }.bind(this));
+  },
+  
+  queryParams: window.location.search.parseQuery(),
+  
+  getTests: function(testcases) {
+    var tests, options = this.options;
+    if (this.queryParams.tests) tests = this.queryParams.tests.split(',');
+    else if (options.tests) tests = options.tests;
+    else if (options.test) tests = [option.test];
+    else tests = Object.keys(testcases).grep(/^test/);
+    
+    return tests.map(function(test) {
+      if (testcases[test])
+        return new Test.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown);
+    }).compact();
+  },
+  
+  getResult: function() {
+    var results = {
+      tests: this.tests.length,
+      assertions: 0,
+      failures: 0,
+      errors: 0
+    };
+    
+    return this.tests.inject(results, function(results, test) {
+      results.assertions += test.assertions;
+      results.failures   += test.failures;
+      results.errors     += test.errors;
+      return results;
+    });
+  },
+  
+  postResults: function() {
+    if (this.options.resultsURL) {
+      new Ajax.Request(this.options.resultsURL, 
+        { method: 'get', parameters: this.getResult(), asynchronous: false });
     }
   },
-  start: function(testName) {
-    if (!this.log) return;
-    this.testName = testName;
-    this.lastLogLine = document.createElement('tr');
-    this.statusCell = document.createElement('td');
-    this.nameCell = document.createElement('td');
-    this.nameCell.appendChild(document.createTextNode(testName));
-    this.messageCell = document.createElement('td');
-    this.lastLogLine.appendChild(this.statusCell);
-    this.lastLogLine.appendChild(this.nameCell);
-    this.lastLogLine.appendChild(this.messageCell);
-    this.loglines.appendChild(this.lastLogLine);
+  
+  runTests: function() {
+    var test = this.tests[this.currentTest], actions;
+    
+    if (!test) return this.finish();
+    if (!test.isWaiting) this.logger.start(test.name);
+    test.run();
+    if (test.isWaiting) {
+      this.logger.message("Waiting for " + test.timeToWait + "ms");
+      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+      return;
+    }
+    
+    this.logger.finish(test.status(), test.summary());
+    if (actions = test.actions) this.logger.appendActionButtons(actions);
+    this.currentTest++;
+    // tail recursive, hopefully the browser will skip the stackframe
+    this.runTests();
+  },
+  
+  finish: function() {
+    this.postResults();
+    this.logger.summary(this.summary());
+  },
+  
+  summary: function() {
+    return '#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors'
+      .interpolate(this.getResult());
+  }
+});
+
+Test.Unit.MessageTemplate = Class.create({
+  initialize: function(string) {
+    var parts = [];
+    (string || '').scan(/(?=[^\\])\?|(?:\\\?|[^\?])+/, function(part) {
+      parts.push(part[0]);
+    });
+    this.parts = parts;
+  },
+  
+  evaluate: function(params) {
+    return this.parts.map(function(part) {
+      return part == '?' ? Test.Unit.inspect(params.shift()) : part.replace(/\\\?/, '?');
+    }).join('');
+  }
+});
+
+Test.Unit.Assertions = {
+  buildMessage: function(message, template) {
+    var args = $A(arguments).slice(2);
+    return (message ? message + '\n' : '') + new Test.Unit.MessageTemplate(template).evaluate(args);
+  },
+  
+  flunk: function(message) {
+    this.assertBlock(message || 'Flunked', function() { return false });
+  },
+  
+  assertBlock: function(message, block) {
+    try {
+      block.call(this) ? this.pass() : this.fail(message);
+    } catch(e) { this.error(e) }
+  },
+  
+  assert: function(expression, message) {
+    message = this.buildMessage(message || 'assert', 'got <?>', expression);
+    this.assertBlock(message, function() { return expression });
+  },
+  
+  assertEqual: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertEqual', 'expected <?>, actual: <?>', expected, actual);
+    this.assertBlock(message, function() { return expected == actual });
   },
-  finish: function(status, summary) {
-    if (!this.log) return;
-    this.lastLogLine.className = status;
-    this.statusCell.innerHTML = status;
-    this.messageCell.innerHTML = this._toHTML(summary);
+  
+  assertNotEqual: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertNotEqual', 'expected <?>, actual: <?>', expected, actual);
+    this.assertBlock(message, function() { return expected != actual });
+  },
+  
+  assertEnumEqual: function(expected, actual, message) {
+    expected = $A(expected);
+    actual = $A(actual);
+    message = this.buildMessage(message || 'assertEnumEqual', 'expected <?>, actual: <?>', expected, actual);
+    this.assertBlock(message, function() {
+      return expected.length == actual.length && expected.zip(actual).all(function(pair) { return pair[0] == pair[1] });
+    });
+  },
+  
+  assertEnumNotEqual: function(expected, actual, message) {
+    expected = $A(expected);
+    actual = $A(actual);
+    message = this.buildMessage(message || 'assertEnumNotEqual', '<?> was the same as <?>', expected, actual);
+    this.assertBlock(message, function() {
+      return expected.length != actual.length || expected.zip(actual).any(function(pair) { return pair[0] != pair[1] });
+    });
   },
-  message: function(message) {
-    if (!this.log) return;
-    this.messageCell.innerHTML = this._toHTML(message);
+  
+  assertHashEqual: function(expected, actual, message) {
+    expected = $H(expected);
+    actual = $H(actual);
+    var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
+    message = this.buildMessage(message || 'assertHashEqual', 'expected <?>, actual: <?>', expected, actual);
+    // from now we recursively zip & compare nested arrays
+    var block = function() {
+      return expected_array.length == actual_array.length && 
+        expected_array.zip(actual_array).all(function(pair) {
+          return pair.all(Object.isArray) ?
+            pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
+        });
+    };
+    this.assertBlock(message, block);
   },
-  summary: function(summary) {
-    if (!this.log) return;
-    this.logsummary.innerHTML = this._toHTML(summary);
+  
+  assertHashNotEqual: function(expected, actual, message) {
+    expected = $H(expected);
+    actual = $H(actual);
+    var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
+    message = this.buildMessage(message || 'assertHashNotEqual', '<?> was the same as <?>', expected, actual);
+    // from now we recursively zip & compare nested arrays
+    var block = function() {
+      return !(expected_array.length == actual_array.length && 
+        expected_array.zip(actual_array).all(function(pair) {
+          return pair.all(Object.isArray) ?
+            pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
+        }));
+    };
+    this.assertBlock(message, block);
+  },
+  
+  assertIdentical: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertIdentical', 'expected <?>, actual: <?>', expected, actual);
+    this.assertBlock(message, function() { return expected === actual });
+  },
+  
+  assertNotIdentical: function(expected, actual, message) { 
+    message = this.buildMessage(message || 'assertNotIdentical', 'expected <?>, actual: <?>', expected, actual);
+    this.assertBlock(message, function() { return expected !== actual });
+  },
+  
+  assertNull: function(obj, message) {
+    message = this.buildMessage(message || 'assertNull', 'got <?>', obj);
+    this.assertBlock(message, function() { return obj === null });
+  },
+  
+  assertNotNull: function(obj, message) {
+    message = this.buildMessage(message || 'assertNotNull', 'got <?>', obj);
+    this.assertBlock(message, function() { return obj !== null });
   },
-  _createLogTable: function() {
-    this.log.innerHTML =
-    '<div id="logsummary"></div>' +
-    '<table id="logtable">' +
-    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
-    '<tbody id="loglines"></tbody>' +
-    '</table>';
-    this.logsummary = $('logsummary')
-    this.loglines = $('loglines');
+  
+  assertUndefined: function(obj, message) {
+    message = this.buildMessage(message || 'assertUndefined', 'got <?>', obj);
+    this.assertBlock(message, function() { return typeof obj == "undefined" });
+  },
+  
+  assertNotUndefined: function(obj, message) {
+    message = this.buildMessage(message || 'assertNotUndefined', 'got <?>', obj);
+    this.assertBlock(message, function() { return typeof obj != "undefined" });
+  },
+  
+  assertNullOrUndefined: function(obj, message) {
+    message = this.buildMessage(message || 'assertNullOrUndefined', 'got <?>', obj);
+    this.assertBlock(message, function() { return obj == null });
+  },
+  
+  assertNotNullOrUndefined: function(obj, message) {
+    message = this.buildMessage(message || 'assertNotNullOrUndefined', 'got <?>', obj);
+    this.assertBlock(message, function() { return obj != null });
+  },
+  
+  assertMatch: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertMatch', 'regex <?> did not match <?>', expected, actual);
+    this.assertBlock(message, function() { return new RegExp(expected).exec(actual) });
   },
-  _toHTML: function(txt) {
-    return txt.escapeHTML().replace(/\n/g,"<br/>");
-  }
-}
+  
+  assertNoMatch: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertNoMatch', 'regex <?> matched <?>', expected, actual);
+    this.assertBlock(message, function() { return !(new RegExp(expected).exec(actual)) });
+  },
+  
+  assertHidden: function(element, message) {
+    message = this.buildMessage(message || 'assertHidden', '? isn\'t hidden.', element);
+    this.assertBlock(message, function() { return element.style.display == 'none' });
+  },
+  
+  assertInstanceOf: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertInstanceOf', '<?> was not an instance of the expected type', actual);
+    this.assertBlock(message, function() { return actual instanceof expected });
+  },
+  
+  assertNotInstanceOf: function(expected, actual, message) {
+    message = this.buildMessage(message || 'assertNotInstanceOf', '<?> was an instance of the expected type', actual);
+    this.assertBlock(message, function() { return !(actual instanceof expected) });
+  },
+  
+  assertRespondsTo: function(method, obj, message) {
+    message = this.buildMessage(message || 'assertRespondsTo', 'object doesn\'t respond to <?>', method);
+    this.assertBlock(message, function() { return (method in obj && typeof obj[method] == 'function') });
+  },
 
-Test.Unit.Runner = Class.create();
-Test.Unit.Runner.prototype = {
-  initialize: function(testcases) {
-    this.options = Object.extend({
-      testLog: 'testlog'
-    }, arguments[1] || {});
-    this.options.resultsURL = this.parseResultsURLQueryParameter();
-    if (this.options.testLog) {
-      this.options.testLog = $(this.options.testLog) || null;
+  assertRaise: function(exceptionName, method, message) {
+    message = this.buildMessage(message || 'assertRaise', '<?> exception expected but none was raised', exceptionName);
+    var block = function() {
+      try { 
+        method();
+        return false;
+      } catch(e) {
+        if (e.name == exceptionName) return true;
+        else throw e;
+      }
+    };
+    this.assertBlock(message, block);
+  },
+  
+  assertNothingRaised: function(method, message) {
+    try { 
+      method();
+      this.assert(true, "Expected nothing to be thrown");
+    } catch(e) {
+      message = this.buildMessage(message || 'assertNothingRaised', '<?> was thrown when nothing was expected.', e);
+      this.flunk(message);
     }
-    if(this.options.tests) {
-      this.tests = [];
-      for(var i = 0; i < this.options.tests.length; i++) {
-        if(/^test/.test(this.options.tests[i])) {
-          this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
-        }
-      }
-    } else {
-      if (this.options.test) {
-        this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
-      } else {
-        this.tests = [];
-        for(var testcase in testcases) {
-          if(/^test/.test(testcase)) {
-            this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
-          }
+  },
+  
+  _isVisible: function(element) {
+    element = $(element);
+    if (!element.parentNode) return true;
+    this.assertNotNull(element);
+    if (element.style && Element.getStyle(element, 'display') == 'none')
+      return false;
+    
+    return arguments.callee.call(this, element.parentNode);
+  },
+  
+  assertVisible: function(element, message) {
+    message = this.buildMessage(message, '? was not visible.', element);
+    this.assertBlock(message, function() { return this._isVisible(element) });
+  },
+  
+  assertNotVisible: function(element, message) {
+    message = this.buildMessage(message, '? was not hidden and didn\'t have a hidden parent either.', element);
+    this.assertBlock(message, function() { return !this._isVisible(element) });
+  },
+  
+  assertElementsMatch: function() {
+    var pass = true, expressions = $A(arguments), elements = $A(expressions.shift());
+    if (elements.length != expressions.length) {
+      message = this.buildMessage('assertElementsMatch', 'size mismatch: ? elements, ? expressions (?).', elements.length, expressions.length, expressions);
+      this.flunk(message);
+      pass = false;
+    }
+    elements.zip(expressions).all(function(pair, index) {
+      var element = $(pair.first()), expression = pair.last();
+      if (element.match(expression)) return true;
+      message = this.buildMessage('assertElementsMatch', 'In index <?>: expected <?> but got ?', index, expression, element);
+      this.flunk(message);
+      pass = false;
+    }.bind(this))
+    
+    if (pass) this.assert(true, "Expected all elements to match.");
+  },
+  
+  assertElementMatches: function(element, expression, message) {
+    this.assertElementsMatch([element], expression);
+  }
+};
+
+Test.Unit.Testcase = Class.create(Test.Unit.Assertions, {
+  initialize: function(name, test, setup, teardown) {
+    this.name           = name;
+    this.test           = test     || Prototype.emptyFunction;
+    this.setup          = setup    || Prototype.emptyFunction;
+    this.teardown       = teardown || Prototype.emptyFunction;
+    this.messages       = [];
+    this.actions        = {};
+  },
+  
+  isWaiting:  false,
+  timeToWait: 1000,
+  assertions: 0,
+  failures:   0,
+  errors:     0,
+  isRunningFromRake: window.location.port == 4711,
+  
+  wait: function(time, nextPart) {
+    this.isWaiting = true;
+    this.test = nextPart;
+    this.timeToWait = time;
+  },
+  
+  run: function(rethrow) {
+    try {
+      try {
+        if (!this.isWaiting) this.setup();
+        this.isWaiting = false;
+        this.test();
+      } finally {
+        if (!this.isWaiting) {
+          this.teardown();
         }
       }
     }
-    this.currentTest = 0;
-    this.logger = new Test.Unit.Logger(this.options.testLog);
-    setTimeout(this.runTests.bind(this), 1000);
-  },
-  parseResultsURLQueryParameter: function() {
-    return window.location.search.parseQuery()["resultsURL"];
-  },
-  // Returns:
-  //  "ERROR" if there was an error,
-  //  "FAILURE" if there was a failure, or
-  //  "SUCCESS" if there was neither
-  getResult: function() {
-    var hasFailure = false;
-    for(var i=0;i<this.tests.length;i++) {
-      if (this.tests[i].errors > 0) {
-        return "ERROR";
-      }
-      if (this.tests[i].failures > 0) {
-        hasFailure = true;
-      }
-    }
-    if (hasFailure) {
-      return "FAILURE";
-    } else {
-      return "SUCCESS";
-    }
-  },
-  postResults: function() {
-    if (this.options.resultsURL) {
-      new Ajax.Request(this.options.resultsURL, 
-        { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
+    catch(e) { 
+      if (rethrow) throw e;
+      this.error(e, this); 
     }
   },
-  runTests: function() {
-    var test = this.tests[this.currentTest];
-    if (!test) {
-      // finished!
-      this.postResults();
-      this.logger.summary(this.summary());
-      return;
-    }
-    if(!test.isWaiting) {
-      this.logger.start(test.name);
-    }
-    test.run();
-    if(test.isWaiting) {
-      this.logger.message("Waiting for " + test.timeToWait + "ms");
-      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
-    } else {
-      this.logger.finish(test.status(), test.summary());
-      var actionButtons = test.actionButtons();
-      if (actionButtons) 
-        $(this.logger.lastLogLine).down('td', 2).appendChild(actionButtons);
-        
-      this.currentTest++;
-      // tail recursive, hopefully the browser will skip the stackframe
-      this.runTests();
-    }
-  },
+  
   summary: function() {
-    var assertions = 0;
-    var failures = 0;
-    var errors = 0;
-    var messages = [];
-    for(var i=0;i<this.tests.length;i++) {
-      assertions +=   this.tests[i].assertions;
-      failures   +=   this.tests[i].failures;
-      errors     +=   this.tests[i].errors;
-    }
-    return (
-      this.tests.length + " tests, " + 
-      assertions + " assertions, " + 
-      failures   + " failures, " +
-      errors     + " errors");
-  }
-}
+    var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors\n';
+    return msg.interpolate(this) + this.messages.join("\n");
+  },
 
-Test.Unit.Assertions = Class.create();
-Test.Unit.Assertions.prototype = {
-  initialize: function() {
-    this.assertions = 0;
-    this.failures   = 0;
-    this.errors     = 0;
-    this.messages   = [];
-    this.actions    = {};
-  },
-  summary: function() {
-    return (
-      this.assertions + " assertions, " + 
-      this.failures   + " failures, " +
-      this.errors     + " errors" + "\n" +
-      this.messages.join("\n"));
-  },
-  actionButtons: function() {
-    if (!Object.keys(this.actions).any()) return false;
-    var div = $(document.createElement("div"));
-    div.addClassName("action_buttons");
-  
-    for (var title in this.actions) {
-      var button = $(document.createElement("input"));
-      button.value = title;
-      button.type = "button";
-      button.observe("click", this.actions[title]);
-      div.appendChild(button);
-    }
-    return div;
-  },
   pass: function() {
     this.assertions++;
   },
+  
   fail: function(message) {
     this.failures++;
-
     var line = "";
     try {
       throw new Error("stack");
     } catch(e){
-      line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1];
-    }   
-
-    this.messages.push("Failure: " + message + (line ? " Line #" + line : ""));
+      match = /.*\/(.*_test\.js):(\d+)/.exec(e.stack || '') || ['','',''];
+      file = match[1];
+      line = match[2];
+    }
+    this.messages.push("Failure: " + message + (line ? " " + file + ": Line #" + line : ""));
   },
+  
   info: function(message) {
     this.messages.push("Info: " + message);
   },
+  
   error: function(error, test) {
     this.errors++;
     this.actions['retry with throw'] = function() { test.run(true) };
     this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) + ")");
   },
+  
   status: function() {
     if (this.failures > 0) return 'failed';
     if (this.errors > 0) return 'error';
     return 'passed';
   },
-  assert: function(expression) {
-    var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
-    try { expression ? this.pass() : 
-      this.fail(message); }
-    catch(e) { this.error(e); }
-  },
-  assertEqual: function(expected, actual) {
-    var message = arguments[2] || "assertEqual";
-    try { (expected == actual) ? this.pass() :
-      this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
-        '", actual "' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNotEqual: function(expected, actual) {
-    var message = arguments[2] || "assertNotEqual";
-    try { (expected != actual) ? this.pass() : 
-      this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertEnumEqual: function(expected, actual) {
-    var message = arguments[2] || "assertEnumEqual";
-    expected = $A(expected);
-    actual = $A(actual);
-    try { expected.length == actual.length && 
-      expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
-        this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
-          ', actual ' + Test.Unit.inspect(actual)); }
-    catch(e) { this.error(e); }
-  },
-  assertEnumNotEqual: function(expected, actual) {
-    var message = arguments[2] || "assertEnumEqual";
-    expected = $A(expected);
-    actual = $A(actual);
-    try { expected.length != actual.length || 
-      expected.zip(actual).any(function(pair) { return pair[0] != pair[1] }) ?
-        this.pass() : this.fail(message + ': ' + Test.Unit.inspect(expected) + 
-          ' was the same as ' + Test.Unit.inspect(actual)); }
-    catch(e) { this.error(e); }
-  },
-  assertHashEqual: function(expected, actual) {
-    var message = arguments[2] || "assertHashEqual";
-    expected = $H(expected);
-    actual = $H(actual);
-    var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
-    // from now we recursively zip & compare nested arrays
-    try { expected_array.length == actual_array.length && 
-      expected_array.zip(actual_array).all(function(pair) {
-        return pair.all(function(i){ return i && i.constructor == Array }) ?
-          pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
-      }) ?
-        this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
-          ', actual ' + Test.Unit.inspect(actual)); }
-    catch(e) { this.error(e); }
-  },
-  assertHashNotEqual: function(expected, actual) {
-    var message = arguments[2] || "assertHashEqual";
-    expected = $H(expected);
-    actual = $H(actual);
-    var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
-    // from now we recursively zip & compare nested arrays
-    try { !(expected_array.length == actual_array.length && 
-      expected_array.zip(actual_array).all(function(pair) {
-        return pair.all(function(i){ return i && i.constructor == Array }) ?
-          pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
-      })) ?
-        this.pass() : this.fail(message + ': ' + Test.Unit.inspect(expected) + 
-          ' was the same as ' + Test.Unit.inspect(actual)); }
-    catch(e) { this.error(e); }
-  },
-  assertIdentical: function(expected, actual) { 
-    var message = arguments[2] || "assertIdentical"; 
-    try { (expected === actual) ? this.pass() : 
-      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
-        '", actual "' + Test.Unit.inspect(actual) + '"'); } 
-    catch(e) { this.error(e); } 
-  },
-  assertNotIdentical: function(expected, actual) { 
-    var message = arguments[2] || "assertNotIdentical"; 
-    try { !(expected === actual) ? this.pass() : 
-      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
-        '", actual "' + Test.Unit.inspect(actual) + '"'); } 
-    catch(e) { this.error(e); } 
-  },
-  assertNull: function(obj) {
-    var message = arguments[1] || 'assertNull'
-    try { (obj===null) ? this.pass() : 
-     this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNotNull: function(obj) {
-    var message = arguments[1] || 'assertNotNull'
-    try { (obj!==null) ? this.pass() : 
-     this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertUndefined: function(obj) {
-    var message = arguments[1] || 'assertUndefined'
-    try { (typeof obj=="undefined") ? this.pass() :
-      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNotUndefined: function(obj) {
-    var message = arguments[1] || 'assertNotUndefined'
-    try { (typeof obj != "undefined") ? this.pass() :
-      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNullOrUndefined: function(obj){
-    var message = arguments[1] || 'assertNullOrUndefined'
-    try { (obj==null) ? this.pass() :
-      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNotNullOrUndefined: function(obj){
-    var message = arguments[1] || 'assertNotNullOrUndefined'
-    try { (obj!=null) ? this.pass() :
-      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertMatch: function(expected, actual) {
-    var message = arguments[2] || 'assertMatch';
-    var regex = new RegExp(expected);
-    try { regex.exec(actual) ? this.pass() :
-      this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNoMatch: function(expected, actual) {
-    var message = arguments[2] || 'assertMatch';
-    var regex = new RegExp(expected);
-    try { !regex.exec(actual) ? this.pass() :
-      this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' matched: ' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertHidden: function(element) {
-    var message = arguments[1] || 'assertHidden';
-    this.assertEqual("none", element.style.display, message);
-  },
-  assertInstanceOf: function(expected, actual) {
-    var message = arguments[2] || 'assertInstanceOf';
-    try { 
-      (actual instanceof expected) ? this.pass() : 
-      this.fail(message + ": object was not an instance of the expected type"); }
-    catch(e) { this.error(e); } 
-  },
-  assertNotInstanceOf: function(expected, actual) {
-    var message = arguments[2] || 'assertNotInstanceOf';
-    try { 
-      !(actual instanceof expected) ? this.pass() : 
-      this.fail(message + ": object was an instance of the not expected type"); }
-    catch(e) { this.error(e); } 
-  },
-  assertRespondsTo: function(method, obj) {
-    var message = arguments[2] || 'assertRespondsTo';
-    try {
-      (obj[method] && typeof obj[method] == 'function') ? this.pass() : 
-      this.fail(message + ": object doesn't respond to [" + method + "]"); }
-    catch(e) { this.error(e); }
-  },
-  assertRaise: function(exceptionName, method) {
-    var message = arguments[2] || 'assertRaise';
-    try { 
-      method();
-      this.fail(message + ": exception expected but none was raised"); }
-    catch(e) {
-      (e.name==exceptionName) ? this.pass() : this.error(e); 
-    }
-  },
-  assertNothingRaised: function(method) {
-    var message = arguments[1] || 'assertNothingRaised';
-    try {
-      method();
-      this.pass();
-    } catch (e) {
-      this.fail(message + ": " + e.toString());
-    }
-  },
-  _isVisible: function(element) {
-    element = $(element);
-    if(!element.parentNode) return true;
-    this.assertNotNull(element);
-    if(element.style && Element.getStyle(element, 'display') == 'none')
-      return false;
-    
-    return this._isVisible(element.parentNode);
-  },
-  assertNotVisible: function(element) {
-    this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
-  },
-  assertVisible: function(element) {
-    this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
-  },
-  assertElementsMatch: function() {
-    var expressions = $A(arguments), elements = $A(expressions.shift());
-    if (elements.length != expressions.length) {
-      this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
-      return false;
-    }
-    elements.zip(expressions).all(function(pair, index) {
-      var element = $(pair.first()), expression = pair.last();
-      if (element.match(expression)) return true;
-      this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
-    }.bind(this)) && this.pass();
-  },
-  assertElementMatches: function(element, expression) {
-    this.assertElementsMatch([element], expression);
-  },
+  
   benchmark: function(operation, iterations) {
     var startAt = new Date();
     (iterations || 1).times(operation);
     var timeTaken = ((new Date())-startAt);
     this.info((arguments[2] || 'Operation') + ' finished ' + 
        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
     return timeTaken;
   }
-}
-
-Test.Unit.Testcase = Class.create();
-Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
-  initialize: function(name, test, setup, teardown) {
-    Test.Unit.Assertions.prototype.initialize.bind(this)();
-    this.name           = name;
-    this.test           = test || function() {};
-    this.setup          = setup || function() {};
-    this.teardown       = teardown || function() {};
-    this.isWaiting      = false;
-    this.timeToWait     = 1000;
-  },
-  wait: function(time, nextPart) {
-    this.isWaiting = true;
-    this.test = nextPart;
-    this.timeToWait = time;
-  },
-  run: function(rethrow) {
-    try {
-      try {
-        if (!this.isWaiting) this.setup.bind(this)();
-        this.isWaiting = false;
-        this.test.bind(this)();
-      } finally {
-        if(!this.isWaiting) {
-          this.teardown.bind(this)();
-        }
-      }
-    }
-    catch(e) { 
-      if (rethrow) throw e;
-      this.error(e, this); 
-    }
-  }
 });
 
 if ( parent.SimpleTest && parent.runAJAXTest ) (function(){
 	var finish = Test.Unit.Logger.prototype.finish;
 	Test.Unit.Logger.prototype.finish = function(status,summary){
 		parent.SimpleTest.ok( status == "passed", this.testName, summary );
 		return finish.apply( this, arguments );
 	};
 
 	// Intentionally overwrite (to stop the Ajax request)
 	Test.Unit.Runner.prototype.postResults = parent.runAJAXTest;
 })();
+
+
--- a/dom/tests/mochitest/ajax/prototype/test/test.css
+++ b/dom/tests/mochitest/ajax/prototype/test/test.css
@@ -7,43 +7,44 @@ body {
 }
 
 #log {
   padding-bottom: 1em;
   border-bottom: 2px solid #000;
   margin-bottom: 2em;
 }
 
-#logsummary {
+.logsummary {
+  margin-top: 1em;
   margin-bottom: 1em;
   padding: 1ex;
   border: 1px solid #000;
   font-weight: bold;
 }
 
-#logtable {
+.logtable {
   width:100%;
   border-collapse: collapse;
   border: 1px dotted #666;
 }
 
-#logtable td, #logtable th {
+.logtable td, .logtable th {
   text-align: left;
   padding: 3px 8px;
   border: 1px dotted #666;
 }
 
-#logtable .passed {
+.logtable .passed {
   background-color: #cfc;
 }
 
-#logtable .failed, #logtable .error {
+.logtable .failed, .logtable .error {
   background-color: #fcc;
 }
 
-#logtable td div.action_buttons {
+.logtable td div.action_buttons {
   display: inline;
 }
 
-#logtable td div.action_buttons input {
+.logtable td div.action_buttons input {
   margin: 0 5px;
   font-size: 10px;
 }
\ No newline at end of file
--- a/dom/tests/mochitest/ajax/prototype/test/unit/Makefile.in
+++ b/dom/tests/mochitest/ajax/prototype/test/unit/Makefile.in
@@ -11,20 +11,21 @@
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is mozilla.org code.
 #
 # The Initial Developer of the Original Code is
 # Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2007
+# Portions created by the Initial Developer are Copyright (C) 2008
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Heather Arthur <harthur@cmu.edu>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
 # or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -38,32 +39,35 @@
 DEPTH		= ../../../../../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= dom/tests/mochitest/ajax/prototype/test/unit
 
 include $(DEPTH)/config/autoconf.mk
 
-DIRS	= \
+DIRS = 	\
 	fixtures \
+	tmp \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
-		ajax.html \
-		array.html \
-		base.html \
-		dom.html \
-		element_mixins.html \
-		enumerable.html \
-		form.html \
-		hash.html \
-		position.html \
-		range.html \
-		selector.html \
-		string.html \
-		unit_tests.html \
-		$(NULL)
+	ajax_test.js \
+	array_test.js \
+	base_test.js \
+	dom_test.js \
+	element_mixins_test.js \
+	enumerable_test.js \
+	event_test.js \
+	form_test.js \
+	hash_test.js \
+	number_test.js \
+	position_test.js \
+	range_test.js \
+	selector_test.js \
+	string_test.js \
+	unittest_test.js \
+	$(NULL)
 
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/prototype/test/unit/ajax.html
+++ /dev/null
@@ -1,174 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-  <title>Prototype Unit test file</title>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-  <script src="../../dist/prototype.js" type="text/javascript"></script>
-  <script src="../lib/unittest.js" type="text/javascript"></script>
-  <link rel="stylesheet" href="../test.css" type="text/css" />
-  <style type="text/css" media="screen">
-  /* <![CDATA[ */
-    #testcss1 { font-size:11px; color: #f00; }
-    #testcss2 { font-size:12px; color: #0f0; display: none; }
-  /* ]]> */
-  </style>
-</head>
-<body>
-<h1>Prototype Unit test file</h1>
-<p>
-  Test of utility functions in ajax.js
-</p>
-
-<!-- Log output -->
-<div id="testlog"> </div>
-<div id="content"></div>
-<div id="content2" style="color:red"></div>
-
-<!-- Tests follow -->
-<script type="text/javascript" language="javascript" charset="utf-8">
-// <![CDATA[
-  var responderCounter = 0;
-
-  new Test.Unit.Runner({
-    
-    setup: function(){
-      $('content').update('');
-      $('content2').update('');
-    },
-    
-    teardown: function(){
-      // hack to cleanup responders
-      Ajax.Responders.responders = [Ajax.Responders.responders[0]];
-    },
-    
-    testSynchronousRequest: function() {with(this) {
-      assertEqual("", $("content").innerHTML);
-      
-      assertEqual(0, Ajax.activeRequestCount);
-      new Ajax.Request("fixtures/hello.js", {
-        asynchronous: false,
-        method: 'GET',
-        onComplete: function(response) { eval(response.responseText) }
-      });
-      assertEqual(0, Ajax.activeRequestCount);
-      
-      var h2 = $("content").firstChild;
-      assertEqual("Hello world!", h2.innerHTML);
-    }},
-    
-    testAsynchronousRequest: function() {with(this) {
-      assertEqual("", $("content").innerHTML);
-      
-      new Ajax.Request("fixtures/hello.js", {
-        asynchronous: true,
-        method: 'get',
-        onComplete: function(response) { eval(response.responseText) }
-      });
-      wait(1000,function(){
-        var h2 = $("content").firstChild;
-        assertEqual("Hello world!", h2.innerHTML);
-      });
-    }},
-    
-    testUpdater: function() {with(this) {
-      assertEqual("", $("content").innerHTML);
-      
-      new Ajax.Updater("content", "fixtures/content.html", { method:'get' });
-      
-      // lowercase comparison because of MSIE which presents HTML tags in uppercase
-      var sentence = ("Pack my box with <em>five dozen</em> liquor jugs! " +
-        "Oh, how <strong>quickly</strong> daft jumping zebras vex...").toLowerCase();
-
-      wait(1000,function(){
-        assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
-        
-        $('content').update('');
-        assertEqual("", $("content").innerHTML);
-         
-        new Ajax.Updater({ success:"content", failure:"content2" },
-          "fixtures/content.html", { method:'get', parameters:{ pet:'monkey' } });
-        
-        new Ajax.Updater("", "fixtures/content.html", { method:'get', parameters:"pet=monkey" });
-        
-        wait(1000,function(){
-          assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
-          assertEqual("", $("content2").innerHTML);
-        });
-      });
-    }},
-    
-    testResponders: function(){with(this) {
-      // check for internal responder
-      assertEqual(1, Ajax.Responders.responders.length);
-      
-      var dummyResponder = {
-        onComplete: function(req){ /* dummy */ }
-      };
-      
-      Ajax.Responders.register(dummyResponder);
-      assertEqual(2, Ajax.Responders.responders.length);
-      
-      // don't add twice
-      Ajax.Responders.register(dummyResponder);
-      assertEqual(2, Ajax.Responders.responders.length);
-      
-      Ajax.Responders.unregister(dummyResponder);
-      assertEqual(1, Ajax.Responders.responders.length);
-      
-      var responder = {
-        onCreate:   function(req){ responderCounter++ },
-        onLoading:  function(req){ responderCounter++ },
-        onComplete: function(req){ responderCounter++ }
-      };
-      Ajax.Responders.register(responder);
-      
-      assertEqual(0, responderCounter);
-      assertEqual(0, Ajax.activeRequestCount);
-      new Ajax.Request("fixtures/content.html", { method:'get', parameters:"pet=monkey" });
-      assertEqual(1, responderCounter);
-      assertEqual(1, Ajax.activeRequestCount);
-      
-      wait(1000,function(){
-        assertEqual(3, responderCounter);
-        assertEqual(0, Ajax.activeRequestCount);
-      });
-    }},
-    
-    testEvalResponseShouldBeCalledBeforeOnComplete: function() {with(this) {
-      assertEqual("", $("content").innerHTML);
-      
-      assertEqual(0, Ajax.activeRequestCount);
-      new Ajax.Request("fixtures/hello.js", {
-        asynchronous: false,
-        method: 'GET',
-        onComplete: function(response) { assertNotEqual("", $("content").innerHTML) }
-      });
-      assertEqual(0, Ajax.activeRequestCount);
-      
-      var h2 = $("content").firstChild;
-      assertEqual("Hello world!", h2.innerHTML);
-    }},
-    
-    testContentTypeSetForSimulatedVerbs: function() {with(this) {
-      var isRunningFromRake = true;
-      
-      new Ajax.Request('/content-type', {
-        method: 'put',
-        contentType: 'application/bogus',
-        asynchronous: false,
-        onFailure: function() {
-          isRunningFromRake = false;
-        },
-        onComplete: function(response) {
-          if (isRunningFromRake)
-            assertEqual('application/bogus; charset=UTF-8', response.responseText);
-        }
-      });
-    }}
-    
-  }, 'testlog');
-// ]]>
-</script>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/unit/ajax_test.js
@@ -0,0 +1,379 @@
+var extendDefault = function(options) {
+  return Object.extend({
+    asynchronous: false,
+    method: 'get',
+    onException: function(e) { throw e }
+  }, options);
+};
+
+new Test.Unit.Runner({
+  setup: function() {
+    $('content').update('');
+    $('content2').update('');
+  },
+  
+  teardown: function() {
+    // hack to cleanup responders
+    Ajax.Responders.responders = [Ajax.Responders.responders[0]];
+  },
+  
+  testSynchronousRequest: function() {
+    this.assertEqual("", $("content").innerHTML);
+    
+    this.assertEqual(0, Ajax.activeRequestCount);
+    new Ajax.Request("../fixtures/hello.js", {
+      asynchronous: false,
+      method: 'GET',
+      evalJS: 'force'
+    });
+    this.assertEqual(0, Ajax.activeRequestCount);
+    
+    var h2 = $("content").firstChild;
+    this.assertEqual("Hello world!", h2.innerHTML);
+  },
+  
+  testAsynchronousRequest: function() {
+    this.assertEqual("", $("content").innerHTML);
+    
+    new Ajax.Request("../fixtures/hello.js", {
+      asynchronous: true,
+      method: 'get',
+      evalJS: 'force'
+    });
+    this.wait(1000, function() {
+      var h2 = $("content").firstChild;
+      this.assertEqual("Hello world!", h2.innerHTML);
+    });
+  },
+  
+  testUpdater: function() {
+    this.assertEqual("", $("content").innerHTML);
+    
+    new Ajax.Updater("content", "../fixtures/content.html", { method:'get' });
+    
+    this.wait(1000, function() {
+      this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
+      
+      $('content').update('');
+      this.assertEqual("", $("content").innerHTML);
+       
+      new Ajax.Updater({ success:"content", failure:"content2" },
+        "../fixtures/content.html", { method:'get', parameters:{ pet:'monkey' } });
+      
+      new Ajax.Updater("", "../fixtures/content.html", { method:'get', parameters:"pet=monkey" });
+      
+      this.wait(1000, function() {
+	// bug 452706        this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
+        this.assertEqual("", $("content2").innerHTML);
+      });
+    }); 
+  },
+  
+  testUpdaterWithInsertion: function() {
+    $('content').update();
+    new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: Insertion.Top });
+    this.wait(1000, function() {
+      // bug 452706       this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
+      $('content').update();
+      new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: 'bottom' });      
+      this.wait(1000, function() {
+        this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
+        
+        $('content').update();
+        new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: 'after' });      
+        this.wait(1000, function() {
+          this.assertEqual('five dozen', $("content").next().innerHTML.strip().toLowerCase());
+        });
+      });
+    });
+  },
+  
+  testUpdaterOptions: function() {
+    var options = {
+      method: 'get',
+      asynchronous: false,
+      evalJS: 'force',
+      onComplete: Prototype.emptyFunction
+    }
+    var request = new Ajax.Updater("content", "../fixtures/hello.js", options);
+    request.options.onComplete = function() {};
+    this.assertIdentical(Prototype.emptyFunction, options.onComplete);
+  },
+  
+  testResponders: function(){
+    // check for internal responder
+    this.assertEqual(1, Ajax.Responders.responders.length);
+    
+    var dummyResponder = {
+      onComplete: function(req) { /* dummy */ }
+    };
+    
+    Ajax.Responders.register(dummyResponder);
+    this.assertEqual(2, Ajax.Responders.responders.length);
+    
+    // don't add twice
+    Ajax.Responders.register(dummyResponder);
+    this.assertEqual(2, Ajax.Responders.responders.length);
+    
+    Ajax.Responders.unregister(dummyResponder);
+    this.assertEqual(1, Ajax.Responders.responders.length);
+    
+    var responder = {
+      onCreate:   function(req){ responderCounter++ },
+      onLoading:  function(req){ responderCounter++ },
+      onComplete: function(req){ responderCounter++ }
+    };
+    Ajax.Responders.register(responder);
+    
+    this.assertEqual(0, responderCounter);
+    this.assertEqual(0, Ajax.activeRequestCount);
+    new Ajax.Request("../fixtures/content.html", { method:'get', parameters:"pet=monkey" });
+    this.assertEqual(1, responderCounter);
+    this.assertEqual(1, Ajax.activeRequestCount);
+    
+    this.wait(1000,function() {
+      this.assertEqual(3, responderCounter);
+      this.assertEqual(0, Ajax.activeRequestCount);
+    });
+  },
+  
+  testEvalResponseShouldBeCalledBeforeOnComplete: function() {
+    if (this.isRunningFromRake) {
+      this.assertEqual("", $("content").innerHTML);
+    
+      this.assertEqual(0, Ajax.activeRequestCount);
+      new Ajax.Request("../fixtures/hello.js", extendDefault({
+        onComplete: function(response) { this.assertNotEqual("", $("content").innerHTML) }.bind(this)
+      }));
+      this.assertEqual(0, Ajax.activeRequestCount);
+    
+      var h2 = $("content").firstChild;
+      this.assertEqual("Hello world!", h2.innerHTML);
+    } else {
+      this.info(message);
+    }
+  },
+  
+  testContentTypeSetForSimulatedVerbs: function() {
+    if (this.isRunningFromRake) {
+      new Ajax.Request('/inspect', extendDefault({
+        method: 'put',
+        contentType: 'application/bogus',
+        onComplete: function(response) {
+          this.assertEqual('application/bogus; charset=UTF-8', response.responseJSON.headers['content-type']);
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+  },
+  
+  testOnCreateCallback: function() {
+    new Ajax.Request("../fixtures/content.html", extendDefault({
+      onCreate: function(transport) { this.assertEqual(0, transport.readyState) }.bind(this),
+      onComplete: function(transport) { this.assertNotEqual(0, transport.readyState) }.bind(this)
+    }));
+  },
+  
+  testEvalJS: function() {
+    if (this.isRunningFromRake) {
+      
+      $('content').update();
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.js,
+        onComplete: function(transport) { 
+          var h2 = $("content").firstChild;
+          this.assertEqual("Hello world!", h2.innerHTML);
+        }.bind(this)
+      }));
+      
+      $('content').update();
+      new Ajax.Request("/response", extendDefault({
+        evalJS: false,
+        parameters: Fixtures.js,
+        onComplete: function(transport) { 
+          this.assertEqual("", $("content").innerHTML);
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+    
+    $('content').update();
+    new Ajax.Request("../fixtures/hello.js", extendDefault({
+      evalJS: 'force',
+      onComplete: function(transport) { 
+        var h2 = $("content").firstChild;
+        this.assertEqual("Hello world!", h2.innerHTML);
+      }.bind(this)
+    }));
+  },
+
+  testCallbacks: function() {
+    var options = extendDefault({
+      onCreate: function(transport) { this.assertInstanceOf(Ajax.Response, transport) }.bind(this)
+    });
+    
+    Ajax.Request.Events.each(function(state){
+      options['on' + state] = options.onCreate;
+    });
+
+    new Ajax.Request("../fixtures/content.html", options);
+  },
+
+  testResponseText: function() {
+    new Ajax.Request("../fixtures/empty.html", extendDefault({
+      onComplete: function(transport) { this.assertEqual('', transport.responseText) }.bind(this)
+    }));
+    
+    new Ajax.Request("../fixtures/content.html", extendDefault({
+      onComplete: function(transport) { this.assertEqual(sentence, transport.responseText.toLowerCase()) }.bind(this)
+    }));
+  },
+  
+  testResponseXML: function() {
+    if (this.isRunningFromRake) {
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.xml,
+        onComplete: function(transport) { 
+          this.assertEqual('foo', transport.responseXML.getElementsByTagName('name')[0].getAttribute('attr'))
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+  },
+      
+  testResponseJSON: function() {
+    if (this.isRunningFromRake) {
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.json,
+        onComplete: function(transport) { this.assertEqual(123, transport.responseJSON.test) }.bind(this)
+      }));
+      
+      new Ajax.Request("/response", extendDefault({
+        parameters: {
+          'Content-Length': 0,
+          'Content-Type': 'application/json'
+        },
+        onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this)
+      }));
+      
+      new Ajax.Request("/response", extendDefault({
+        evalJSON: false,
+        parameters: Fixtures.json,
+        onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this)
+      }));
+    
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.jsonWithoutContentType,
+        onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this)
+      }));
+    
+      new Ajax.Request("/response", extendDefault({
+        sanitizeJSON: true,
+        parameters: Fixtures.invalidJson,
+        onException: function(request, error) {
+          this.assert(error.message.include('Badly formed JSON string'));
+          this.assertInstanceOf(Ajax.Request, request);
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+    
+    new Ajax.Request("../fixtures/data.json", extendDefault({
+      evalJSON: 'force',
+      onComplete: function(transport) { this.assertEqual(123, transport.responseJSON.test) }.bind(this)
+    }));
+  },
+  
+  testHeaderJSON: function() {
+    if (this.isRunningFromRake) {
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.headerJson,
+        onComplete: function(transport, json) {
+          this.assertEqual('hello #éà', transport.headerJSON.test);
+          this.assertEqual('hello #éà', json.test);
+        }.bind(this)
+      }));
+    
+      new Ajax.Request("/response", extendDefault({
+        onComplete: function(transport, json) { 
+          this.assertNull(transport.headerJSON)
+          this.assertNull(json)
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+  },
+  
+  testGetHeader: function() {
+    if (this.isRunningFromRake) {
+     new Ajax.Request("/response", extendDefault({
+        parameters: { 'X-TEST': 'some value' },
+        onComplete: function(transport) {
+          this.assertEqual('some value', transport.getHeader('X-Test'));
+          this.assertNull(transport.getHeader('X-Inexistant'));
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+  },
+  
+  testParametersCanBeHash: function() {
+    if (this.isRunningFromRake) {
+      new Ajax.Request("/response", extendDefault({
+        parameters: $H({ "one": "two", "three": "four" }),
+        onComplete: function(transport) {
+          this.assertEqual("two", transport.getHeader("one"));
+          this.assertEqual("four", transport.getHeader("three"));
+          this.assertNull(transport.getHeader("toObject"));
+        }.bind(this)
+      }));
+    } else {
+      this.info(message);
+    }
+  },
+  
+  testIsSameOriginMethod: function() {
+    var isSameOrigin = Ajax.Request.prototype.isSameOrigin;
+    this.assert(isSameOrigin.call({ url: '/foo/bar.html' }), '/foo/bar.html');
+    this.assert(isSameOrigin.call({ url: window.location.toString() }), window.location);
+    this.assert(!isSameOrigin.call({ url: 'http://example.com' }), 'http://example.com');
+
+    if (this.isRunningFromRake) {
+      Ajax.Request.prototype.isSameOrigin = function() {
+        return false
+      };
+
+      $("content").update('same origin policy');
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.js,
+        onComplete: function(transport) { 
+          this.assertEqual("same origin policy", $("content").innerHTML);
+        }.bind(this)
+      }));
+
+      new Ajax.Request("/response", extendDefault({
+        parameters: Fixtures.invalidJson,
+        onException: function(request, error) {
+          this.assert(error.message.include('Badly formed JSON string'));
+        }.bind(this)
+      }));
+
+      new Ajax.Request("/response", extendDefault({
+        parameters: { 'X-JSON': '{});window.attacked = true;({}' },
+        onException: function(request, error) {
+          this.assert(error.message.include('Badly formed JSON string'));
+        }.bind(this)
+      }));
+
+      Ajax.Request.prototype.isSameOrigin = isSameOrigin;
+    } else {
+      this.info(message);
+    }
+  }
+});
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/prototype/test/unit/array.html
+++ /dev/null
@@ -1,178 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-  <title>Prototype Unit test file</title>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-  <script src="../../dist/prototype.js" type="text/javascript"></script>
-  <script src="../lib/unittest.js" type="text/javascript"></script>
-  <link rel="stylesheet" href="../test.css" type="text/css" />
-  <style type="text/css" media="screen">
-  /* <![CDATA[ */
-    #testcss1 { font-size:11px; color: #f00; }
-    #testcss2 { font-size:12px; color: #0f0; display: none; }
-  /* ]]> */
-  </style>
-</head>
-<body>
-<h1>Prototype Unit test file</h1>
-<p>
-  Test of the Array.prototype extensions
-</p>
-
-<!-- Log output -->
-<div id="testlog"> </div>
-
-<div id="test_node">22<span id="span_1"></span><span id="span_2"></span></div>
-
-<!-- Tests follow -->
-<script type="text/javascript" language="javascript" charset="utf-8">
-// <![CDATA[
-
-  var globalArgsTest = 'nothing to see here';
-
-  new Test.Unit.Runner({
-    
-    testToArrayOnArguments: function(){ with(this) {
-      function toArrayOnArguments(){
-        globalArgsTest = $A(arguments);
-      }
-      toArrayOnArguments();
-      assertEnumEqual([], globalArgsTest);
-      toArrayOnArguments('foo');
-      assertEnumEqual(['foo'], globalArgsTest);
-      toArrayOnArguments('foo','bar');
-      assertEnumEqual(['foo','bar'], globalArgsTest);
-    }},
-    
-    testToArrayOnNodeList: function(){ with(this) {
-      // direct HTML
-      assertEqual(3, $A($('test_node').childNodes).length);
-      
-      // DOM
-      var element = document.createElement('div');
-      element.appendChild(document.createTextNode('22'));
-      (2).times(function(){ element.appendChild(document.createElement('span')) });
-      assertEqual(3, $A(element.childNodes).length);
-      
-      // HTML String
-      element = document.createElement('div');
-      $(element).update('22<span></span><span></span');
-      assertEqual(3, $A(element.childNodes).length);
-    }},
-    
-    testClear: function(){ with(this) {
-      assertEnumEqual([], [].clear());
-      assertEnumEqual([], [1].clear());
-      assertEnumEqual([], [1,2].clear());
-    }},
-    
-    testClone: function(){ with(this) {
-      assertEnumEqual([], [].clone());
-      assertEnumEqual([1], [1].clone());
-      assertEnumEqual([1,2], [1,2].clone());
-      assertEnumEqual([0,1,2], [0,1,2].clone());
-      var a = [0,1,2];
-      var b = a;
-      assertIdentical(a, b);
-      b = a.clone();
-      assertNotIdentical(a, b);
-    }},
-    
-    testFirst: function(){ with(this) {
-      assertUndefined([].first());
-      assertEqual(1, [1].first());
-      assertEqual(1, [1,2].first());
-    }},
-    
-    testLast: function(){ with(this) {
-      assertUndefined([].last());
-      assertEqual(1, [1].last());
-      assertEqual(2, [1,2].last());
-    }},
-    
-    testCompact: function(){ with(this) {
-      assertEnumEqual([],      [].compact());
-      assertEnumEqual([1,2,3], [1,2,3].compact());
-      assertEnumEqual([0,1,2,3], [0,null,1,2,undefined,3].compact());
-      assertEnumEqual([1,2,3], [null,1,2,3,null].compact());
-    }},
-    
-    testFlatten: function(){ with(this) {
-      assertEnumEqual([],      [].flatten());
-      assertEnumEqual([1,2,3], [1,2,3].flatten());
-      assertEnumEqual([1,2,3], [1,[[[2,3]]]].flatten());
-      assertEnumEqual([1,2,3], [[1],[2],[3]].flatten());
-      assertEnumEqual([1,2,3], [[[[[[[1]]]]]],2,3].flatten());
-    }},
-    
-    testIndexOf: function(){ with(this) {
-      assertEqual(-1, [].indexOf(1));
-      assertEqual(-1, [0].indexOf(1));
-      assertEqual(0, [1].indexOf(1));
-      assertEqual(1, [0,1,2].indexOf(1));
-    }},
-    
-    testInspect: function(){ with(this) {
-      assertEqual('[]',[].inspect());
-      assertEqual('[1]',[1].inspect());
-      assertEqual('[\'a\']',['a'].inspect());
-      assertEqual('[\'a\', 1]',['a',1].inspect());
-    }},
-    
-    testToJSON: function(){ with(this) {
-      assertEqual('[]', [].toJSON());
-      assertEqual('[\"a\"]', ['a'].toJSON());
-      assertEqual('[\"a\",1]', ['a', 1].toJSON());
-      assertEqual('[\"a\",{\"b\":null}]', ['a', {'b': null}].toJSON());
-    }},
-        
-    testReduce: function(){ with(this) {
-      assertUndefined([].reduce());
-      assertNull([null].reduce());
-      assertEqual(1, [1].reduce());
-      assertEnumEqual([1,2,3], [1,2,3].reduce());
-      assertEnumEqual([1,null,3], [1,null,3].reduce());
-    }},
-    
-    testReverse: function(){ with(this) {
-      assertEnumEqual([], [].reverse());
-      assertEnumEqual([1], [1].reverse());
-      assertEnumEqual([2,1], [1,2].reverse());
-      assertEnumEqual([3,2,1], [1,2,3].reverse());
-    }},
-    
-    testSize: function(){ with(this) {
-      assertEqual(4, [0, 1, 2, 3].size());
-      assertEqual(0, [].size());
-    }},
-
-    testUniq: function(){ with(this) {
-      assertEnumEqual([1], [1, 1, 1].uniq());
-      assertEnumEqual([1], [1].uniq());
-      assertEnumEqual([], [].uniq());
-      assertEnumEqual([0, 1, 2, 3], [0, 1, 2, 2, 3, 0, 2].uniq());
-      assertEnumEqual([0, 1, 2, 3], [0, 0, 1, 1, 2, 3, 3, 3].uniq(true));
-    }},
-    
-    testWithout: function(){ with(this) {
-      assertEnumEqual([], [].without(0));
-      assertEnumEqual([], [0].without(0));
-      assertEnumEqual([1], [0,1].without(0));
-      assertEnumEqual([1,2], [0,1,2].without(0));
-    }},
-    
-    test$w: function(){ with(this) {
-      assertEnumEqual(['a', 'b', 'c', 'd'], $w('a b c d'));
-      assertEnumEqual([], $w(' '));
-      assertEnumEqual(['a'], $w('a'));
-      assertEnumEqual(['a'], $w('a '));
-      assertEnumEqual(['a'], $w(' a'));
-      assertEnumEqual(['a', 'b', 'c', 'd'], $w(' a   b\nc\t\nd\n'));
-    }}
-
-  }, 'testlog');
-// ]]>
-</script>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/unit/array_test.js
@@ -0,0 +1,190 @@
+var globalArgsTest = 'nothing to see here';
+
+new Test.Unit.Runner({
+  test$A: function(){
+    this.assertEnumEqual([], $A({}));
+  },
+  
+  testToArrayOnArguments: function(){
+    function toArrayOnArguments(){
+      globalArgsTest = $A(arguments);
+    }
+    toArrayOnArguments();
+    this.assertEnumEqual([], globalArgsTest);
+    toArrayOnArguments('foo');
+    this.assertEnumEqual(['foo'], globalArgsTest);
+    toArrayOnArguments('foo','bar');
+    this.assertEnumEqual(['foo','bar'], globalArgsTest);
+  },
+  
+  testToArrayOnNodeList: function(){
+    // direct HTML
+    this.assertEqual(3, $A($('test_node').childNodes).length);
+    
+    // DOM
+    var element = document.createElement('div');
+    element.appendChild(document.createTextNode('22'));
+    (2).times(function(){ element.appendChild(document.createElement('span')) });
+    this.assertEqual(3, $A(element.childNodes).length);
+    
+    // HTML String
+    element = document.createElement('div');
+    $(element).update('22<span></span><span></span');
+    this.assertEqual(3, $A(element.childNodes).length);
+  },
+  
+  testClear: function(){
+    this.assertEnumEqual([], [].clear());
+    this.assertEnumEqual([], [1].clear());
+    this.assertEnumEqual([], [1,2].clear());
+  },
+  
+  testClone: function(){
+    this.assertEnumEqual([], [].clone());
+    this.assertEnumEqual([1], [1].clone());
+    this.assertEnumEqual([1,2], [1,2].clone());
+    this.assertEnumEqual([0,1,2], [0,1,2].clone());
+    var a = [0,1,2];
+    var b = a;
+    this.assertIdentical(a, b);
+    b = a.clone();
+    this.assertNotIdentical(a, b);
+  },
+  
+  testFirst: function(){
+    this.assertUndefined([].first());
+    this.assertEqual(1, [1].first());
+    this.assertEqual(1, [1,2].first());
+  },
+  
+  testLast: function(){
+    this.assertUndefined([].last());
+    this.assertEqual(1, [1].last());
+    this.assertEqual(2, [1,2].last());
+  },
+  
+  testCompact: function(){
+    this.assertEnumEqual([],      [].compact());
+    this.assertEnumEqual([1,2,3], [1,2,3].compact());
+    this.assertEnumEqual([0,1,2,3], [0,null,1,2,undefined,3].compact());
+    this.assertEnumEqual([1,2,3], [null,1,2,3,null].compact());
+  },
+  
+  testFlatten: function(){
+    this.assertEnumEqual([],      [].flatten());
+    this.assertEnumEqual([1,2,3], [1,2,3].flatten());
+    this.assertEnumEqual([1,2,3], [1,[[[2,3]]]].flatten());
+    this.assertEnumEqual([1,2,3], [[1],[2],[3]].flatten());
+    this.assertEnumEqual([1,2,3], [[[[[[[1]]]]]],2,3].flatten());
+  },
+  
+  testIndexOf: function(){
+    this.assertEqual(-1, [].indexOf(1));
+    this.assertEqual(-1, [0].indexOf(1));
+    this.assertEqual(0, [1].indexOf(1));
+    this.assertEqual(1, [0,1,2].indexOf(1));
+    this.assertEqual(0, [1,2,1].indexOf(1));
+    this.assertEqual(2, [1,2,1].indexOf(1, -1));
+    this.assertEqual(1, [undefined,null].indexOf(null));
+  },
+
+  testLastIndexOf: function(){
+    this.assertEqual(-1,[].lastIndexOf(1));
+    this.assertEqual(-1, [0].lastIndexOf(1));
+    this.assertEqual(0, [1].lastIndexOf(1));
+    this.assertEqual(2, [0,2,4,6].lastIndexOf(4));
+    this.assertEqual(3, [4,4,2,4,6].lastIndexOf(4));
+    this.assertEqual(3, [0,2,4,6].lastIndexOf(6,3));
+    this.assertEqual(-1, [0,2,4,6].lastIndexOf(6,2));
+    this.assertEqual(0, [6,2,4,6].lastIndexOf(6,2));
+    
+    var fixture = [1,2,3,4,3];
+    this.assertEqual(4, fixture.lastIndexOf(3));
+    this.assertEnumEqual([1,2,3,4,3],fixture);
+    
+    //tests from http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf
+    var array = [2, 5, 9, 2];
+    this.assertEqual(3,array.lastIndexOf(2));
+    this.assertEqual(-1,array.lastIndexOf(7));
+    this.assertEqual(3,array.lastIndexOf(2,3));
+    this.assertEqual(0,array.lastIndexOf(2,2));
+    this.assertEqual(0,array.lastIndexOf(2,-2));
+    this.assertEqual(3,array.lastIndexOf(2,-1));
+  },
+  
+  testInspect: function(){
+    this.assertEqual('[]',[].inspect());
+    this.assertEqual('[1]',[1].inspect());
+    this.assertEqual('[\'a\']',['a'].inspect());
+    this.assertEqual('[\'a\', 1]',['a',1].inspect());
+  },
+  
+  testIntersect: function(){
+    this.assertEnumEqual([1,3], [1,1,3,5].intersect([1,2,3]));
+    this.assertEnumEqual([1], [1,1].intersect([1,1]));
+    this.assertEnumEqual([0], [0,2].intersect([1,0]));
+    this.assertEnumEqual([], [1,1,3,5].intersect([4]));
+    this.assertEnumEqual([], [1].intersect(['1']));
+    
+    this.assertEnumEqual(
+      ['B','C','D'], 
+      $R('A','Z').toArray().intersect($R('B','D').toArray())
+    );
+  },
+  
+  testToJSON: function(){
+    this.assertEqual('[]', [].toJSON());
+    this.assertEqual('[\"a\"]', ['a'].toJSON());
+    this.assertEqual('[\"a\", 1]', ['a', 1].toJSON());
+    this.assertEqual('[\"a\", {\"b\": null}]', ['a', {'b': null}].toJSON());
+  },
+      
+  testReduce: function(){
+    this.assertUndefined([].reduce());
+    this.assertNull([null].reduce());
+    this.assertEqual(1, [1].reduce());
+    this.assertEnumEqual([1,2,3], [1,2,3].reduce());
+    this.assertEnumEqual([1,null,3], [1,null,3].reduce());
+  },
+  
+  testReverse: function(){
+    this.assertEnumEqual([], [].reverse());
+    this.assertEnumEqual([1], [1].reverse());
+    this.assertEnumEqual([2,1], [1,2].reverse());
+    this.assertEnumEqual([3,2,1], [1,2,3].reverse());
+  },
+  
+  testSize: function(){
+    this.assertEqual(4, [0, 1, 2, 3].size());
+    this.assertEqual(0, [].size());
+  },
+
+  testUniq: function(){
+    this.assertEnumEqual([1], [1, 1, 1].uniq());
+    this.assertEnumEqual([1], [1].uniq());
+    this.assertEnumEqual([], [].uniq());
+    this.assertEnumEqual([0, 1, 2, 3], [0, 1, 2, 2, 3, 0, 2].uniq());
+    this.assertEnumEqual([0, 1, 2, 3], [0, 0, 1, 1, 2, 3, 3, 3].uniq(true));
+  },
+  
+  testWithout: function(){
+    this.assertEnumEqual([], [].without(0));
+    this.assertEnumEqual([], [0].without(0));
+    this.assertEnumEqual([1], [0,1].without(0));
+    this.assertEnumEqual([1,2], [0,1,2].without(0));
+  },
+  
+  test$w: function(){
+    this.assertEnumEqual(['a', 'b', 'c', 'd'], $w('a b c d'));
+    this.assertEnumEqual([], $w(' '));
+    this.assertEnumEqual([], $w(''));
+    this.assertEnumEqual([], $w(null));
+    this.assertEnumEqual([], $w(undefined));
+    this.assertEnumEqual([], $w());
+    this.assertEnumEqual([], $w(10));
+    this.assertEnumEqual(['a'], $w('a'));
+    this.assertEnumEqual(['a'], $w('a '));
+    this.assertEnumEqual(['a'], $w(' a'));
+    this.assertEnumEqual(['a', 'b', 'c', 'd'], $w(' a   b\nc\t\nd\n'));
+  }
+});
\ No newline at end of file
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/prototype/test/unit/base.html
+++ /dev/null
@@ -1,234 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-  <title>Prototype Unit test file</title>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-  <script src="../../dist/prototype.js" type="text/javascript"></script>
-  <script src="../lib/unittest.js" type="text/javascript"></script>
-  <link rel="stylesheet" href="../test.css" type="text/css" />
-  <style type="text/css" media="screen">
-  /* <![CDATA[ */
-    #testcss1 { font-size:11px; color: #f00; }
-    #testcss2 { font-size:12px; color: #0f0; display: none; }
-  /* ]]> */
-  </style>
-</head>
-<body>
-<h1>Prototype Unit test file</h1>
-<p>
-  Test of utility functions in base.js
-</p>
-
-<!-- Log output -->
-<div id="testlog"> </div>
-<div id="test"></div> 
-<!-- Tests follow -->
-<script type="text/javascript" language="javascript" charset="utf-8">
-// <![CDATA[
-  var Person = function(name){
-      this.name = name;
-  };
-  
-  Person.prototype.toJSON = function() {
-    return '-' + this.name;
-  };
-
-  var peEventCount = 0;
-  // peEventFired will stop the PeriodicalExecuter after 3 callbacks
-  function peEventFired(pe) {
-    if (++peEventCount>2) {
-      pe.stop();
-    }
-  }
-  
-  var arg1 = 1;
-  var arg2 = 2;
-  var arg3 = 3;
-  function TestObj(){}
-  TestObj.prototype.assertingEventHandler = 
-    function( event, assertEvent, assert1, assert2, assert3, a1, a2, a3 ){
-      assertEvent(event);
-      assert1(a1);
-      assert2(a2);
-      assert3(a3);
-    }
-    
-  var globalBindTest = null;
-
-  new Test.Unit.Runner({
-    
-    testFunctionBind: function() { with(this) {
-      function methodWithoutArguments(){
-        globalBindTest = this.hi;
-      }
-      function methodWithArguments(){
-        globalBindTest = this.hi + ',' + $A(arguments).join(',');
-      }
-      function methodWithBindArguments(){
-        globalBindTest = this.hi + ',' + $A(arguments).join(',');
-      }
-      function methodWithBindArgumentsAndArguments(){
-        globalBindTest = this.hi + ',' + $A(arguments).join(',');
-      }
-      
-      methodWithoutArguments.bind({hi:'without'})();
-      assertEqual('without', globalBindTest);
-      
-      methodWithArguments.bind({hi:'with'})('arg1','arg2');
-      assertEqual('with,arg1,arg2', globalBindTest);
-      
-      methodWithBindArguments.bind({hi:'withBindArgs'},'arg1','arg2')();
-      assertEqual('withBindArgs,arg1,arg2', globalBindTest);
-      
-      methodWithBindArgumentsAndArguments.bind({hi:'withBindArgsAndArgs'},'arg1','arg2')('arg3','arg4');
-      assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4', globalBindTest);
-    }},
-
-    testObjectInspect: function() { with(this) {
-      assertEqual('undefined', Object.inspect());
-      assertEqual('undefined', Object.inspect(undefined));
-      assertEqual('null', Object.inspect(null));
-      assertEqual("'foo\\\\b\\\'ar'", Object.inspect('foo\\b\'ar'));
-      assertEqual('[]', Object.inspect([]));
-    }},
-    
-    testObjectToJSON: function() { with(this) {
-      assertUndefined(Object.toJSON(undefined));
-      assertUndefined(Object.toJSON(Prototype.K));
-      assertEqual('\"\"', Object.toJSON(''));
-      assertEqual('[]', Object.toJSON([]));
-      assertEqual('[\"a\"]', Object.toJSON(['a']));
-      assertEqual('[\"a\",1]', Object.toJSON(['a', 1]));
-      assertEqual('[\"a\",{\"b\":null}]', Object.toJSON(['a', {'b': null}]));
-      assertEqual('{\"a\":\"hello!\"}', Object.toJSON({a: 'hello!'}));
-      assertEqual('{}', Object.toJSON({}));
-      assertEqual('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K}));
-      assertEqual('{\"b\":[false,true],\"c\":{\"a\":\"hello!\"}}',
-        Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}));
-      assertEqual('{\"b\":[false,true],\"c\":{\"a\":\"hello!\"}}',
-        Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})));
-      assertEqual('true', Object.toJSON(true));
-      assertEqual('false', Object.toJSON(false));
-      assertEqual('null', Object.toJSON(null));
-      var sam = new Person('sam');
-      assertEqual('-sam', Object.toJSON(sam));
-      assertEqual('-sam', sam.toJSON());
-      var element = $('test');
-      assertUndefined(Object.toJSON(element));
-      element.toJSON = function(){return 'I\'m a div with id test'};
-      assertEqual('I\'m a div with id test', Object.toJSON(element));
-    }},
-    
-    // sanity check
-    testDoesntExtendObjectPrototype: function() {with(this) {
-      // for-in is supported with objects
-      var iterations = 0, obj = { a: 1, b: 2, c: 3 };
-      for(property in obj) iterations++;
-      assertEqual(3, iterations);
-      
-      // for-in is not supported with arrays
-      iterations = 0;
-      var arr = [1,2,3];
-      for(property in arr) iterations++;
-      assert(iterations > 3);
-    }},
-    
-    testPeriodicalExecuterStop: function() {with(this) {
-      new PeriodicalExecuter(peEventFired, 0.1);
-      
-      wait(1000, function() {
-          assertEqual(3, peEventCount);
-      });
-    }},
-
-    testBindAsEventListener: function() {
-      for( var i = 0; i < 10; ++i ){
-        var div = document.createElement('div');
-        div.setAttribute('id','test-'+i);
-        document.body.appendChild(div);
-        var tobj = new TestObj();
-        var eventTest = {test:true};
-        var call = tobj.assertingEventHandler.bindAsEventListener(tobj,
-          this.assertEqual.bind(this,eventTest),
-          this.assertEqual.bind(this,arg1),
-          this.assertEqual.bind(this,arg2),
-          this.assertEqual.bind(this,arg3), arg1, arg2, arg3 );
-        call(eventTest);
-      }
-    },
-    
-    testNumberToColorPart: function() {with(this) {
-      assertEqual('00', (0).toColorPart());
-      assertEqual('0a', (10).toColorPart());
-      assertEqual('ff', (255).toColorPart());
-    }},
-    
-    testNumberToPaddedString: function() {with(this) {
-      assertEqual('00', (0).toPaddedString(2, 16));
-      assertEqual('0a', (10).toPaddedString(2, 16));
-      assertEqual('ff', (255).toPaddedString(2, 16));
-      assertEqual('000', (0).toPaddedString(3));
-      assertEqual('010', (10).toPaddedString(3));
-      assertEqual('100', (100).toPaddedString(3));
-      assertEqual('1000', (1000).toPaddedString(3));
-    }},
-    
-    testNumberToJSON: function() {with(this) {
-      assertEqual('null', Number.NaN.toJSON());
-      assertEqual('0', (0).toJSON());
-      assertEqual('-293', (-293).toJSON());
-    }},
-    
-    testDateToJSON: function() {with(this) {
-      assertEqual('\"1969-12-31T19:00:00\"', new Date(1969, 11, 31, 19).toJSON());
-    }},
-    
-    testBrowserDetection: function() {with(this) {
-      var results = $H(Prototype.Browser).map(function(engine){
-        return engine;
-      }).partition(function(engine){
-        return engine[1] === true
-      });
-      var trues = results[0], falses = results[1];
-      
-      info('User agent string is: ' + navigator.userAgent);
-      
-      assert(trues.size() == 0 || trues.size() == 1, 
-        'There should be only one or no browser detected.');
-      
-      // we should have definite trues or falses here
-      trues.each(function(result){
-        assert(result[1] === true);
-      });
-      falses.each(function(result){
-        assert(result[1] === false);
-      });
-      
-      if(navigator.userAgent.indexOf('AppleWebKit/') > -1) {
-        info('Running on WebKit');
-        assert(Prototype.Browser.WebKit);
-      }
-      
-      if(!!window.opera) {
-        info('Running on Opera');
-        assert(Prototype.Browser.Opera);
-      }
-      
-      if(!!(window.attachEvent && !window.opera)) {
-        info('Running on IE');
-        assert(Prototype.Browser.IE);
-      }
-      
-      if(navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1) {
-        info('Running on Gecko');
-        assert(Prototype.Browser.Gecko);
-      } 
-    }}
-
-  }, 'testlog');
-
-// ]]>
-</script>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/prototype/test/unit/base_test.js
@@ -0,0 +1,511 @@
+new Test.Unit.Runner({
+  testFunctionArgumentNames: function() {
+    this.assertEnumEqual([], (function() {}).argumentNames());
+    this.assertEnumEqual(["one"], (function(one) {}).argumentNames());
+    this.assertEnumEqual(["one", "two", "three"], (function(one, two, three) {}).argumentNames());
+    this.assertEnumEqual(["one", "two", "three"], (function(  one  , two 
+       , three   ) {}).argumentNames());
+    this.assertEqual("$super", (function($super) {}).argumentNames().first());
+    
+    function named1() {};
+    this.assertEnumEqual([], named1.argumentNames());
+    function named2(one) {};
+    this.assertEnumEqual(["one"], named2.argumentNames());
+    function named3(one, two, three) {};
+    this.assertEnumEqual(["one", "two", "three"], named3.argumentNames());
+  },
+  
+  testFunctionBind: function() {
+    function methodWithoutArguments() { return this.hi };
+    function methodWithArguments()    { return this.hi + ',' + $A(arguments).join(',') };
+    var func = Prototype.emptyFunction;
+
+    this.assertIdentical(func, func.bind());
+    this.assertIdentical(func, func.bind(undefined));
+    this.assertNotIdentical(func, func.bind(null));
+
+    this.assertEqual('without', methodWithoutArguments.bind({ hi: 'without' })());
+    this.assertEqual('with,arg1,arg2', methodWithArguments.bind({ hi: 'with' })('arg1','arg2'));
+    this.assertEqual('withBindArgs,arg1,arg2',
+      methodWithArguments.bind({ hi: 'withBindArgs' }, 'arg1', 'arg2')());
+    this.assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4',
+      methodWithArguments.bind({ hi: 'withBindArgsAndArgs' }, 'arg1', 'arg2')('arg3', 'arg4'));
+  },
+  
+  testFunctionCurry: function() {
+    var split = function(delimiter, string) { return string.split(delimiter); };
+    var splitOnColons = split.curry(":");
+    this.assertNotIdentical(split, splitOnColons);
+    this.assertEnumEqual(split(":", "0:1:2:3:4:5"), splitOnColons("0:1:2:3:4:5"));
+    this.assertIdentical(split, split.curry());
+  },
+  
+  testFunctionDelay: function() {
+    window.delayed = undefined;
+    var delayedFunction = function() { window.delayed = true; };
+    var delayedFunctionWithArgs = function() { window.delayedWithArgs = $A(arguments).join(' '); };
+    delayedFunction.delay(0.8);
+    delayedFunctionWithArgs.delay(0.8, 'hello', 'world');
+    this.assertUndefined(window.delayed);
+    this.wait(1000, function() {
+      this.assert(window.delayed);
+      this.assertEqual('hello world', window.delayedWithArgs);
+    });
+  },
+  
+  testFunctionWrap: function() {
+    function sayHello(){
+      return 'hello world';
+    };
+    
+    this.assertEqual('HELLO WORLD', sayHello.wrap(function(proceed) {
+      return proceed().toUpperCase();
+    })());
+    
+    var temp = String.prototype.capitalize;
+    String.prototype.capitalize = String.prototype.capitalize.wrap(function(proceed, eachWord) {
+      if (eachWord && this.include(' ')) return this.split(' ').map(function(str){
+        return str.capitalize();
+      }).join(' ');
+      return proceed();
+    });
+    this.assertEqual('Hello world', 'hello world'.capitalize());
+    this.assertEqual('Hello World', 'hello world'.capitalize(true));
+    this.assertEqual('Hello', 'hello'.capitalize());
+    String.prototype.capitalize = temp;
+  },
+  
+  testFunctionDefer: function() {
+    window.deferred = undefined;
+    var deferredFunction = function() { window.deferred = true; };
+    deferredFunction.defer();
+    this.assertUndefined(window.deferred);      
+    this.wait(50, function() {
+      this.assert(window.deferred);
+      
+      window.deferredValue = 0;
+      var deferredFunction2 = function(arg) { window.deferredValue = arg; };
+      deferredFunction2.defer('test');
+      this.wait(50, function() {
+        this.assertEqual('test', window.deferredValue);
+      });
+    });
+  },
+  
+  testFunctionMethodize: function() {
+    var Foo = { bar: function(baz) { return baz } };
+    var baz = { quux: Foo.bar.methodize() };
+    
+    this.assertEqual(Foo.bar.methodize(), baz.quux);
+    this.assertEqual(baz, Foo.bar(baz));
+    this.assertEqual(baz, baz.quux());
+  },
+
+  testObjectExtend: function() {
+    var object = {foo: 'foo', bar: [1, 2, 3]};
+    this.assertIdentical(object, Object.extend(object));
+    this.assertHashEqual({foo: 'foo', bar: [1, 2, 3]}, object);
+    this.assertIdentical(object, Object.extend(object, {bla: 123}));
+    this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: 123}, object);
+    this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: null},
+      Object.extend(object, {bla: null}));
+  },
+  
+  testObjectToQueryString: function() {
+    this.assertEqual('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'}));
+  },
+  
+  testObjectClone: function() {
+    var object = {foo: 'foo', bar: [1, 2, 3]};
+    this.assertNotIdentical(object, Object.clone(object));
+    this.assertHashEqual(object, Object.clone(object));
+    this.assertHashEqual({}, Object.clone());
+    var clone = Object.clone(object);
+    delete clone.bar;
+    this.assertHashEqual({foo: 'foo'}, clone, 
+      "Optimizing Object.clone perf using prototyping doesn't allow properties to be deleted.");
+  },
+
+  testObjectInspect: function() {
+    this.assertEqual('undefined', Object.inspect());
+    this.assertEqual('undefined', Object.inspect(undefined));
+    this.assertEqual('null', Object.inspect(null));
+    this.assertEqual("'foo\\\\b\\\'ar'", Object.inspect('foo\\b\'ar'));
+    this.assertEqual('[]', Object.inspect([]));
+    this.assertNothingRaised(function() { Object.inspect(window.Node) });
+  },
+  
+  testObjectToJSON: function() {
+    this.assertUndefined(Object.toJSON(undefined));
+    this.assertUndefined(Object.toJSON(Prototype.K));
+    this.assertEqual('\"\"', Object.toJSON(''));
+    this.assertEqual('[]', Object.toJSON([]));
+    this.assertEqual('[\"a\"]', Object.toJSON(['a']));
+    this.assertEqual('[\"a\", 1]', Object.toJSON(['a', 1]));
+    this.assertEqual('[\"a\", {\"b\": null}]', Object.toJSON(['a', {'b': null}]));
+    this.assertEqual('{\"a\": \"hello!\"}', Object.toJSON({a: 'hello!'}));
+    this.assertEqual('{}', Object.toJSON({}));
+    this.assertEqual('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K}));
+    this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}',
+      Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}));
+    this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}',
+      Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})));
+    this.assertEqual('true', Object.toJSON(true));
+    this.assertEqual('false', Object.toJSON(false));
+    this.assertEqual('null', Object.toJSON(null));
+    var sam = new Person('sam');
+    this.assertEqual('-sam', Object.toJSON(sam));
+    this.assertEqual('-sam', sam.toJSON());
+    var element = $('test');
+    this.assertUndefined(Object.toJSON(element));
+    element.toJSON = function(){return 'I\'m a div with id test'};
+    this.assertEqual('I\'m a div with id test', Object.toJSON(element));
+  },
+  
+  testObjectToHTML: function() {
+    this.assertIdentical('', Object.toHTML());
+    this.assertIdentical('', Object.toHTML(''));
+    this.assertIdentical('', Object.toHTML(null));
+    this.assertIdentical('0', Object.toHTML(0));
+    this.assertIdentical('123', Object.toHTML(123));
+    this.assertEqual('hello world', Object.toHTML('hello world'));
+    this.assertEqual('hello world', Object.toHTML({toHTML: function() { return 'hello world' }}));
+  },
+  
+  testObjectIsArray: function() {
+    this.assert(Object.isArray([]));
+    this.assert(Object.isArray([0]));
+    this.assert(Object.isArray([0, 1]));
+    this.assert(!Object.isArray({}));
+    this.assert(!Object.isArray($('list').childNodes));
+    this.assert(!Object.isArray());
+    this.assert(!Object.isArray(''));
+    this.assert(!Object.isArray('foo'));
+    this.assert(!Object.isArray(0));
+    this.assert(!Object.isArray(1));
+    this.assert(!Object.isArray(null));
+    this.assert(!Object.isArray(true));
+    this.assert(!Object.isArray(false));
+    this.assert(!Object.isArray(undefined));
+  },
+  
+  testObjectIsHash: function() {
+    this.assert(Object.isHash($H()));
+    this.assert(Object.isHash(new Hash()));
+    this.assert(!Object.isHash({}));
+    this.assert(!Object.isHash(null));
+    this.assert(!Object.isHash());
+    this.assert(!Object.isHash(''));
+    this.assert(!Object.isHash(2));
+    this.assert(!Object.isHash(false));
+    this.assert(!Object.isHash(true));
+    this.assert(!Object.isHash([]));
+  },
+  
+  testObjectIsElement: function() {
+    this.assert(Object.isElement(document.createElement('div')));
+    this.assert(Object.isElement(new Element('div')));
+    this.assert(Object.isElement($('testlog')));
+    this.assert(!Object.isElement(document.createTextNode('bla')));
+
+    // falsy variables should not mess up return value type
+    this.assertIdentical(false, Object.isElement(0));
+    this.assertIdentical(false, Object.isElement(''));
+    this.assertIdentical(false, Object.isElement(NaN));
+    this.assertIdentical(false, Object.isElement(null));
+    this.assertIdentical(false, Object.isElement(undefined));
+  },
+  
+  testObjectIsFunction: function() {
+    this.assert(Object.isFunction(function() { }));
+    this.assert(Object.isFunction(Class.create()));
+    this.assert(!Object.isFunction("a string"));
+    this.assert(!Object.isFunction($("testlog")));
+    this.assert(!Object.isFunction([]));
+    this.assert(!Object.isFunction({}));
+    this.assert(!Object.isFunction(0));
+    this.assert(!Object.isFunction(false));
+    this.assert(!Object.isFunction(undefined));
+    this.assert(!Object.isFunction(/foo/));
+    this.assert(!Object.isFunction(document.getElementsByTagName('div')));
+  },
+  
+  testObjectIsString: function() {
+    this.assert(!Object.isString(function() { }));
+    this.assert(Object.isString("a string"));
+    this.assert(!Object.isString(0));
+    this.assert(!Object.isString([]));
+    this.assert(!Object.isString({}));
+    this.assert(!Object.isString(false));
+    this.assert(!Object.isString(undefined));
+  },
+  
+  testObjectIsNumber: function() {
+    this.assert(Object.isNumber(0));
+    this.assert(Object.isNumber(1.0));
+    this.assert(!Object.isNumber(function() { }));
+    this.assert(!Object.isNumber("a string"));
+    this.assert(!Object.isNumber([]));
+    this.assert(!Object.isNumber({}));
+    this.assert(!Object.isNumber(false));
+    this.assert(!Object.isNumber(undefined));
+  },
+  
+  testObjectIsUndefined: function() {
+    this.assert(Object.isUndefined(undefined));
+    this.assert(!Object.isUndefined(null));
+    this.assert(!Object.isUndefined(false));
+    this.assert(!Object.isUndefined(0));
+    this.assert(!Object.isUndefined(""));
+    this.assert(!Object.isUndefined(function() { }));
+    this.assert(!Object.isUndefined([]));
+    this.assert(!Object.isUndefined({}));
+  },
+  
+  // sanity check
+  testDoesntExtendObjectPrototype: function() {
+    // for-in is supported with objects
+    var iterations = 0, obj = { a: 1, b: 2, c: 3 };
+    for (property in obj) iterations++;
+    this.assertEqual(3, iterations);
+    
+    // for-in is not supported with arrays
+    iterations = 0;
+    var arr = [1,2,3];
+    for (property in arr) iterations++;
+    this.assert(iterations > 3);
+  },
+  
+
+  testBindAsEventListener: function() {
+    for ( var i = 0; i < 10; ++i ) {
+      var div = document.createElement('div');
+      div.setAttribute('id','test-'+i);
+      document.body.appendChild(div);
+      var tobj = new TestObj();
+      var eventTest = { test: true };
+      var call = tobj.assertingEventHandler.bindAsEventListener(tobj,
+        this.assertEqual.bind(this, eventTest),
+        this.assertEqual.bind(this, arg1),
+        this.assertEqual.bind(this, arg2),
+        this.assertEqual.bind(this, arg3), arg1, arg2, arg3 );
+      call(eventTest);
+    }
+  },
+  
+  testDateToJSON: function() {
+    this.assertEqual('\"1970-01-01T00:00:00Z\"', new Date(Date.UTC(1970, 0, 1)).toJSON());
+  },
+  
+  testRegExpEscape: function() {
+    this.assertEqual('word', RegExp.escape('word'));
+    this.assertEqual('\\/slashes\\/', RegExp.escape('/slashes/'));
+    this.assertEqual('\\\\backslashes\\\\', RegExp.escape('\\backslashes\\'));
+    this.assertEqual('\\\\border of word', RegExp.escape('\\border of word'));
+    
+    this.assertEqual('\\(\\?\\:non-capturing\\)', RegExp.escape('(?:non-capturing)'));
+    this.assertEqual('non-capturing', new RegExp(RegExp.escape('(?:') + '([^)]+)').exec('(?:non-capturing)')[1]);
+    
+    this.assertEqual('\\(\\?\\=positive-lookahead\\)', RegExp.escape('(?=positive-lookahead)'));
+    this.assertEqual('positive-lookahead', new RegExp(RegExp.escape('(?=') + '([^)]+)').exec('(?=positive-lookahead)')[1]);
+    
+    this.assertEqual('\\(\\?<\\=positive-lookbehind\\)', RegExp.escape('(?<=positive-lookbehind)'));
+    this.assertEqual('positive-lookbehind', new RegExp(RegExp.escape('(?<=') + '([^)]+)').exec('(?<=positive-lookbehind)')[1]);
+    
+    this.assertEqual('\\(\\?\\!negative-lookahead\\)', RegExp.escape('(?!negative-lookahead)'));
+    this.assertEqual('negative-lookahead', new RegExp(RegExp.escape('(?!') + '([^)]+)').exec('(?!negative-lookahead)')[1]);
+    
+    this.assertEqual('\\(\\?<\\!negative-lookbehind\\)', RegExp.escape('(?<!negative-lookbehind)'));
+    this.assertEqual('negative-lookbehind', new RegExp(RegExp.escape('(?<!') + '([^)]+)').exec('(?<!negative-lookbehind)')[1]);
+    
+    this.assertEqual('\\[\\\\w\\]\\+', RegExp.escape('[\\w]+'));
+    this.assertEqual('character class', new RegExp(RegExp.escape('[') + '([^\\]]+)').exec('[character class]')[1]);      
+    
+    this.assertEqual('<div>', new RegExp(RegExp.escape('<div>')).exec('<td><div></td>')[0]);      
+    
+    this.assertEqual('false', RegExp.escape(false));
+    this.assertEqual('undefined', RegExp.escape());
+    this.assertEqual('null', RegExp.escape(null));
+    this.assertEqual('42', RegExp.escape(42));
+    
+    this.assertEqual('\\\\n\\\\r\\\\t', RegExp.escape('\\n\\r\\t'));
+    this.assertEqual('\n\r\t', RegExp.escape('\n\r\t'));
+    this.assertEqual('\\{5,2\\}', RegExp.escape('{5,2}'));
+    
+    this.assertEqual(
+      '\\/\\(\\[\\.\\*\\+\\?\\^\\=\\!\\:\\$\\{\\}\\(\\)\\|\\[\\\\\\]\\\\\\\/\\\\\\\\\\]\\)\\/g',
+      RegExp.escape('/([.*+?^=!:${}()|[\\]\\/\\\\])/g')
+    );
+  },
+  
+  testBrowserDetection: function() {
+    var results = $H(Prototype.Browser).map(function(engine){
+      return engine;
+    }).partition(function(engine){
+      return engine[1] === true
+    });
+    var trues = results[0], falses = results[1];
+    
+    this.info('User agent string is: ' + navigator.userAgent);
+    
+    this.assert(trues.size() == 0 || trues.size() == 1, 
+      'There should be only one or no browser detected.');
+    
+    // we should have definite trues or falses here
+    trues.each(function(result) {
+      this.assert(result[1] === true);
+    }, this);
+    falses.each(function(result) {
+      this.assert(result[1] === false);
+    }, this);
+    
+    if (navigator.userAgent.indexOf('AppleWebKit/') > -1) {
+      this.info('Running on WebKit');
+      this.assert(Prototype.Browser.WebKit);
+    }
+    
+    if (!!window.opera) {
+      this.info('Running on Opera');
+      this.assert(Prototype.Browser.Opera);
+    }
+    
+    if (!!(window.attachEvent && !window.opera)) {
+      this.info('Running on IE');
+      this.assert(Prototype.Browser.IE);
+    }
+    
+    if (navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1) {
+      this.info('Running on Gecko');
+      this.assert(Prototype.Browser.Gecko);
+    } 
+  },
+  
+  testClassCreate: function() { 
+    this.assert(Object.isFunction(Animal), 'Animal is not a constructor');
+    this.assertEnumEqual([Cat, Mouse, Dog, Ox], Animal.subclasses);
+    Animal.subclasses.each(function(subclass) {
+      this.assertEqual(Animal, subclass.superclass);
+    }, this);
+
+    var Bird = Class.create(Animal);
+    this.assertEqual(Bird, Animal.subclasses.last());
+    // for..in loop (for some reason) doesn't iterate over the constructor property in top-level classes
+    this.assertEnumEqual(Object.keys(new Animal).sort(), Object.keys(new Bird).without('constructor').sort());
+  },
+
+  testClassInstantiation: function() { 
+    var pet = new Animal("Nibbles");
+    this.assertEqual("Nibbles", pet.name, "property not initialized");
+    this.assertEqual('Nibbles: Hi!', pet.say('Hi!'));
+    this.assertEqual(Animal, pet.constructor, "bad constructor reference");
+    this.assertUndefined(pet.superclass);
+
+    var Empty = Class.create();
+    this.assert('object', typeof new Empty);
+  },
+
+  testInheritance: function() {
+    var tom = new Cat('Tom');
+    this.assertEqual(Cat, tom.constructor, "bad constructor reference");
+    this.assertEqual(Animal, tom.constructor.superclass, 'bad superclass reference');
+    this.assertEqual('Tom', tom.name);
+    this.assertEqual('Tom: meow', tom.say('meow'));
+    this.assertEqual('Tom: Yuk! I only eat mice.', tom.eat(new Animal));
+  },
+
+  testSuperclassMethodCall: function() {
+    var tom = new Cat('Tom');
+    this.assertEqual('Tom: Yum!', tom.eat(new Mouse));
+
+    // augment the constructor and test
+    var Dodo = Class.create(Animal, {
+      initialize: function($super, name) {
+        $super(name);
+        this.extinct = true;
+      },
+      
+      say: function($super, message) {
+        return $super(message) + " honk honk";
+      }
+    });
+
+    var gonzo = new Dodo('Gonzo');
+    this.assertEqual('Gonzo', gonzo.name);
+    this.assert(gonzo.extinct, 'Dodo birds should be extinct');
+    this.assertEqual("Gonzo: hello honk honk", gonzo.say("hello"));
+  },
+
+  testClassAddMethods: function() {
+    var tom   = new Cat('Tom');
+    var jerry = new Mouse('Jerry');
+    
+    Animal.addMethods({
+      sleep: function() {
+        return this.say('ZZZ');
+      }
+    });
+    
+    Mouse.addMethods({
+      sleep: function($super) {
+        return $super() + " ... no, can't sleep! Gotta steal cheese!";
+      },
+      escape: function(cat) {
+        return this.say('(from a mousehole) Take that, ' + cat.name + '!');
+      }
+    });
+    
+    this.assertEqual('Tom: ZZZ', tom.sleep(), "added instance method not available to subclass");
+    this.assertEqual("Jerry: ZZZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep());
+    this.assertEqual("Jerry: (from a mousehole) Take that, Tom!", jerry.escape(tom));
+    // insure that a method has not propagated *up* the prototype chain:
+    this.assertUndefined(tom.escape);
+    this.assertUndefined(new Animal().escape);
+    
+    Animal.addMethods({
+      sleep: function() {
+        return this.say('zZzZ');
+      }
+    });
+    
+    this.assertEqual("Jerry: zZzZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep());
+  },
+  
+  testBaseClassWithMixin: function() {
+    var grass = new Plant('grass', 3);
+    this.assertRespondsTo('getValue', grass);      
+    this.assertEqual('#<Plant: grass>', grass.inspect());
+  },
+  
+  testSubclassWithMixin: function() {
+    var snoopy = new Dog('Snoopy', 12, 'male');
+    this.assertRespondsTo('reproduce', snoopy);      
+  },
+ 
+  testSubclassWithMixins: function() {
+    var cow = new Ox('cow', 400, 'female');
+    this.assertEqual('#<Ox: cow>', cow.inspect());
+    this.assertRespondsTo('reproduce', cow);
+    this.assertRespondsTo('getValue', cow);
+  },
+ 
+  testClassWithToStringAndValueOfMethods: function() {
+    var Foo = Class.create({