DanielWagnerHall on behalf of SeveralGooglers: Pushing out a bunch of Google javascript changes

* Added support for new IE10 pointer events
* Moved attribute/property conflating from atoms to webdriver atoms
* Lots of linting and type fixing
* Made bot.action.getInteractableSize public
* Added ability to force events to fire on non-interactable elements
* Added HTML5 doctype declarations to all test pages
* Always use webdriver.atoms.inject not bot.inject, as this is required by the
android driver
* Updated closure library and compiler to the latest release

git-svn-id: http://selenium.googlecode.com/svn/trunk@18101 07704840-8298-11de-bf8c-fd130f914ac9
diff --git a/cpp/prebuilt/amd64/libnoblur64.so b/cpp/prebuilt/amd64/libnoblur64.so
index 916e530..8f96aea 100755
--- a/cpp/prebuilt/amd64/libnoblur64.so
+++ b/cpp/prebuilt/amd64/libnoblur64.so
Binary files differ
diff --git a/cpp/prebuilt/i386/libnoblur.so b/cpp/prebuilt/i386/libnoblur.so
index 8e7db8d..ae7dcee 100755
--- a/cpp/prebuilt/i386/libnoblur.so
+++ b/cpp/prebuilt/i386/libnoblur.so
Binary files differ
diff --git a/java/client/src/org/openqa/selenium/firefox/build.desc b/java/client/src/org/openqa/selenium/firefox/build.desc
index 28f8370..aa4d6b9 100644
--- a/java/client/src/org/openqa/selenium/firefox/build.desc
+++ b/java/client/src/org/openqa/selenium/firefox/build.desc
@@ -13,9 +13,9 @@
     "//third_party/java/commons-exec",

   ],

   embedded = [

-    "//javascript/firefox-driver:webdriver",

-    "//javascript/firefox-driver:webdriver_prefs",

     ":amd64",

+    ":firefox_extension",

+    ":webdriver_prefs",

     ":x86",

   ])

 

@@ -39,6 +39,20 @@
     "x86/libibushandler.so",

   ])

 

+rename(

+    name = "webdriver_prefs",

+    out = "webdriver_prefs.json",

+    deps = [

+        "//javascript/firefox-driver:webdriver_prefs",

+    ])

+

+rename(

+    name = "firefox_extension",

+    out = "webdriver.xpi",

+    deps = [

+        "//javascript/firefox-driver:webdriver",

+    ])

+

 # TODO(dawagner): These tmp folders are a horrible hack, and should disappear post haste

 rename(name = "noblur",

   deps = [

diff --git a/javascript/atoms/action.js b/javascript/atoms/action.js
index 873d892..1985a58 100644
--- a/javascript/atoms/action.js
+++ b/javascript/atoms/action.js
@@ -469,7 +469,7 @@
 bot.action.multiTouchAction_ = function(element, transformStart, transformHalf,
                                         opt_coords, opt_touchscreen) {
   var center = bot.action.prepareToInteractWith_(element, opt_coords);
-  var size = bot.action.getInteractableSize_(element);
+  var size = bot.action.getInteractableSize(element);
   var offsetVec = new goog.math.Vec2(
       Math.min(center.x, size.width - center.x),
       Math.min(center.y, size.height - center.y));
@@ -528,7 +528,7 @@
   if (opt_coords) {
     return goog.math.Vec2.fromCoordinate(opt_coords);
   } else {
-    var size = bot.action.getInteractableSize_(element);
+    var size = bot.action.getInteractableSize(element);
     return new goog.math.Vec2(size.width / 2, size.height / 2);
   }
 };
@@ -539,12 +539,11 @@
  *
  * @param {!Element} elem Element.
  * @return {!goog.math.Size} size Size of the element.
- * @private
  */
-bot.action.getInteractableSize_ = function(elem) {
+bot.action.getInteractableSize = function(elem) {
   var size = goog.style.getSize(elem);
   return ((size.width > 0 && size.height > 0) || !elem.offsetParent) ? size :
-      bot.action.getInteractableSize_(elem.offsetParent);
+      bot.action.getInteractableSize(elem.offsetParent);
 };
 
 
diff --git a/javascript/atoms/bot.js b/javascript/atoms/bot.js
index c94ea31..5eb25fa 100644
--- a/javascript/atoms/bot.js
+++ b/javascript/atoms/bot.js
@@ -29,6 +29,8 @@
  * @type {!Window}
  * @private
  */
+bot.window_;
+
 try {
   bot.window_ = window;
 } catch (ignored) {
diff --git a/javascript/atoms/color.js b/javascript/atoms/color.js
index 8d78de1..0808741 100644
--- a/javascript/atoms/color.js
+++ b/javascript/atoms/color.js
@@ -20,8 +20,8 @@
 
 goog.provide('bot.color');
 
+goog.require('goog.array');
 goog.require('goog.color.names');
-goog.require('goog.math');
 
 
 /**
diff --git a/javascript/atoms/device.js b/javascript/atoms/device.js
index 1a5e684..6e1a228 100644
--- a/javascript/atoms/device.js
+++ b/javascript/atoms/device.js
@@ -31,6 +31,7 @@
 goog.require('goog.userAgent.product');
 
 
+
 /**
  * A Device class that provides common functionality for input devices.
  * @param {bot.Device.ModifiersState=} opt_modifiersState state of modifier
@@ -132,15 +133,16 @@
  * @param {!goog.math.Coordinate} coord The coordinate where event will fire.
  * @param {number} button The mouse button value for the event.
  * @param {Element=} opt_related The related element of this event.
- * @param {number=} opt_wheelDelta The wheel delta value for the event.
+ * @param {?number=} opt_wheelDelta The wheel delta value for the event.
+ * @param {boolean=} opt_force Whether the event should be fired even if the
+ *     element is not interactable, such as the case of a mousemove or
+ *     mouseover event that immediately follows a mouseout.
  * @return {boolean} Whether the event fired successfully; false if cancelled.
  * @protected
  */
 bot.Device.prototype.fireMouseEvent = function(type, coord, button,
-                                               opt_related, opt_wheelDelta) {
-  // TODO(user): Event if the element is not interactable, the mouse event
-  // should still fire on another element (offset parent?).
-  if (!bot.dom.isInteractable(this.element_)) {
+    opt_related, opt_wheelDelta, opt_force) {
+  if (!opt_force && !bot.dom.isInteractable(this.element_)) {
     return false;
   }
 
@@ -227,6 +229,63 @@
 
 
 /**
+ * Fires a MSPointer event given the state of the device and the given
+ * arguments.
+ *
+ * @param {bot.events.EventType} type MSPointer event type.
+ * @param {!goog.math.Coordinate} coord The coordinate where event will fire.
+ * @param {number} button The mouse button value for the event.
+ * @param {number} pointerId The pointer id for this event.
+ * @param {number} device The device type used for this event.
+ * @param {boolean} isPrimary Whether the pointer represents the primary point
+ *     of contact.
+ * @param {Element=} opt_related The related element of this event.
+ * @param {boolean=} opt_force Whether the event should be fired even if the
+ *     element is not interactable, such as the case of a mousemove or
+ *     mouseover event that immediately follows a mouseout.
+ * @return {boolean} Whether the event fired successfully; false if cancelled.
+ * @protected
+ */
+bot.Device.prototype.fireMSPointerEvent = function(type, coord, button,
+    pointerId, device, isPrimary, opt_related, opt_force) {
+  if (!opt_force && !bot.dom.isInteractable(this.element_)) {
+    return false;
+  }
+
+  if (opt_related &&
+      !(bot.events.EventType.MSPOINTEROVER == type ||
+        bot.events.EventType.MSPOINTEROUT == type)) {
+    throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
+                        'Event type does not allow related target: ' + type);
+  }
+
+  var args = {
+    clientX: coord.x,
+    clientY: coord.y,
+    button: button,
+    altKey: false,
+    ctrlKey: false,
+    shiftKey: false,
+    metaKey: false,
+    relatedTarget: opt_related || null,
+    width: 0,
+    height: 0,
+    pressure: 0, // Pressure is only given when a stylus is used.
+    rotation: 0,
+    pointerId: pointerId,
+    tiltX: 0,
+    tiltY: 0,
+    pointerType: device,
+    isPrimary: isPrimary
+  };
+
+  var target = this.select_ ?
+      this.getTargetOfOptionMouseEvent_(type) : this.element_;
+  return target ? bot.events.fire(target, type, args) : true;
+};
+
+
+/**
  * A mouse event fired "on" an <option> element, doesn't always fire on the
  * <option> element itself. Sometimes it fires on the parent <select> element
  * and sometimes not at all, depending on the browser and event type. This
@@ -241,9 +300,11 @@
   if (goog.userAgent.IE) {
     switch (type) {
       case bot.events.EventType.MOUSEOVER:
+      case bot.events.EventType.MSPOINTEROVER:
         return null;
       case bot.events.EventType.CONTEXTMENU:
       case bot.events.EventType.MOUSEMOVE:
+      case bot.events.EventType.MSPOINTERMOVE:
         return this.select_.multiple ? this.select_ : null;
       default:
         return this.select_;
@@ -427,7 +488,7 @@
  */
 bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ =
     goog.userAgent.WEBKIT || goog.userAgent.OPERA ||
-      (bot.userAgent.FIREFOX_EXTENSION && bot.userAgent.isProductVersion(3.6));
+    (bot.userAgent.FIREFOX_EXTENSION && bot.userAgent.isProductVersion(3.6));
 
 
 /**
@@ -691,6 +752,7 @@
 };
 
 
+
 /**
  * Stores the state of modifier keys
  *
diff --git a/javascript/atoms/dom.js b/javascript/atoms/dom.js
index 2e75331..b30b045 100644
--- a/javascript/atoms/dom.js
+++ b/javascript/atoms/dom.js
@@ -43,7 +43,8 @@
  * @return {Element} The active element, if any.
  */
 bot.dom.getActiveElement = function(nodeOrWindow) {
-  return goog.dom.getOwnerDocument(nodeOrWindow).activeElement;
+  return goog.dom.getActiveElement(
+      goog.dom.getOwnerDocument(nodeOrWindow));
 };
 
 
@@ -53,7 +54,7 @@
  * is an element, regardless of the tag name.h
  *
  * @param {Node} node The node to test.
- * @param {goog.dom.TagName=} opt_tagName Tag name to test the node for.
+ * @param {string=} opt_tagName Tag name to test the node for.
  * @return {boolean} Whether the node is an element with the given tag name.
  */
 bot.dom.isElement = function(node, opt_tagName) {
@@ -74,11 +75,23 @@
 bot.dom.isInteractable = function(element) {
   return bot.dom.isShown(element, /*ignoreOpacity=*/true) &&
       bot.dom.isEnabled(element) &&
-      // check pointer-style isn't 'none'
-      // Although IE, Opera, FF < 3.6 don't care about this property.
-      (goog.userAgent.IE || goog.userAgent.OPERA ||
-          (bot.userAgent.FIREFOX_EXTENSION && bot.userAgent.isProductVersion(3.6)) ||
-        bot.dom.getEffectiveStyle(element, 'pointer-events') != 'none');
+      !bot.dom.hasPointerEventsDisabled_(element);
+};
+
+
+/**
+ * @param {!Element} element Element.
+ * @return {boolean} Whether element is set by the CSS pointer-events property
+ *     not to be interactable.
+ * @private
+ */
+bot.dom.hasPointerEventsDisabled_ = function(element) {
+  if (goog.userAgent.IE || goog.userAgent.OPERA ||
+      (goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {
+    // Don't support pointer events
+    return false;
+  }
+  return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';
 };
 
 
@@ -157,127 +170,27 @@
 
 
 /**
- * Common aliases for properties. This maps names that users use to the correct
- * property name.
- *
- * @const
- * @private
- */
-bot.dom.PROPERTY_ALIASES_ = {
-  'class': 'className',
-  'readonly': 'readOnly'
-};
-
-
-/**
- * A list of boolean properties that are defined for all elements
- * according to the HTML5 spec. If any of these are missing when
- * calling 'getProperty' they default to false.
- *
- * http://dev.w3.org/html5/spec/Overview.html#elements-in-the-dom
- *
- * @const
- * @private
- */
-bot.dom.BOOLEAN_PROPERTIES_ = [
-  'checked',
-  'disabled',
-  'draggable',
-  'hidden'
-];
-
-
-/**
  * Looks up the given property (not to be confused with an attribute) on the
- * given element. The following properties are aliased so that they return the
- * values expected by users:
- *
- * <ul>
- * <li>class - as "className"
- * <li>readonly - as "readOnly"
- * </ul>
+ * given element.
  *
  * @param {!Element} element The element to use.
  * @param {string} propertyName The name of the property.
  * @return {*} The value of the property.
  */
 bot.dom.getProperty = function(element, propertyName) {
-  var key = bot.dom.PROPERTY_ALIASES_[propertyName] || propertyName;
-
-  var value = element[key];
-  if (!goog.isDef(value) &&
-      goog.array.contains(bot.dom.BOOLEAN_PROPERTIES_, key)) {
-    return false;
-  }
-
-  if (propertyName == 'value' &&
+  // When an <option>'s value attribute is not set, its value property should be
+  // its text content, but IE < 8 does not adhere to that behavior, so fix it.
+  // http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION
+  if (bot.userAgent.IE_DOC_PRE8 && propertyName == 'value' &&
       bot.dom.isElement(element, goog.dom.TagName.OPTION) &&
-      !bot.dom.hasAttribute(element, propertyName)) {
-    // IE does not adhere to this behaviour, so we hack it in. See:
-    // http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION
-    value = goog.dom.getRawTextContent(element);
+      goog.isNull(bot.dom.getAttribute(element, 'value'))) {
+    return goog.dom.getRawTextContent(element);
   }
-  return value;
+  return element[propertyName];
 };
 
 
 /**
- * Used to determine whether we should return a boolean value from getAttribute.
- * These are all extracted from the WHATWG spec:
- *
- *   http://www.whatwg.org/specs/web-apps/current-work/
- *
- * These must all be lower-case.
- *
- * @const
- * @private
- */
-bot.dom.BOOLEAN_ATTRIBUTES_ = [
-  'async',
-  'autofocus',
-  'autoplay',
-  'checked',
-  'compact',
-  'complete',
-  'controls',
-  'declare',
-  'defaultchecked',
-  'defaultselected',
-  'defer',
-  'disabled',
-  'draggable',
-  'ended',
-  'formnovalidate',
-  'hidden',
-  'indeterminate',
-  'iscontenteditable',
-  'ismap',
-  'itemscope',
-  'loop',
-  'multiple',
-  'muted',
-  'nohref',
-  'noresize',
-  'noshade',
-  'novalidate',
-  'nowrap',
-  'open',
-  'paused',
-  'pubdate',
-  'readonly',
-  'required',
-  'reversed',
-  'scoped',
-  'seamless',
-  'seeking',
-  'selected',
-  'spellcheck',
-  'truespeed',
-  'willvalidate'
-];
-
-
-/**
  * Regex to split on semicolons, but not when enclosed in parens or quotes.
  * Helper for {@link bot.dom.standardizeStyleAttribute_}.
  * If the style attribute ends with a semicolon this will include an empty
@@ -294,15 +207,6 @@
 
 
 /**
- * @param {string} attributeName The name of the attribute to check.
- * @return {boolean} Whether the specified attribute is a boolean attribute.
- */
-bot.dom.isBooleanAttribute = function(attributeName) {
-    return goog.array.contains(bot.dom.BOOLEAN_ATTRIBUTES_, attributeName);
-};
-
-
-/**
  * Standardize a style attribute value, which includes:
  *  (1) converting all property names lowercase
  *  (2) ensuring it ends in a trailing semi-colon
@@ -333,23 +237,26 @@
 
 /**
  * Get the user-specified value of the given attribute of the element, or null
- * if no such value. This method endeavours to return consistent values between
- * browsers. For boolean attributes such as "selected" or "checked", it returns
- * the string "true" if it is present and null if it is not. For the style
- * attribute, it standardizes the value by lower-casing the property names
- * and always including a trailing semi-colon.
+ * if the attribute is not present.
+ *
+ * <p>For boolean attributes such as "selected" or "checked", this method
+ * returns the value of element.getAttribute(attributeName) cast to a String
+ * when attribute is present. For modern browsers, this will be the string the
+ * attribute is given in the HTML, but for IE8 it will be the name of the
+ * attribute, and for IE7, it will be the string "true". To test whether a
+ * boolean attribute is present, test whether the return value is non-null, the
+ * same as one would for non-boolean attributes. Specifically, do *not* test
+ * whether the boolean evaluation of the return value is true, because the value
+ * of a boolean attribute that is present will often be the empty string.
+ *
+ * <p>For the style attribute, it standardizes the value by lower-casing the
+ * property names and always including a trailing semi-colon.
  *
  * @param {!Element} element The element to use.
  * @param {string} attributeName The name of the attribute to return.
  * @return {?string} The value of the attribute or "null" if entirely missing.
  */
 bot.dom.getAttribute = function(element, attributeName) {
-  // Protect ourselves from the case where documentElementsByTagName also
-  // returns comments in IE.
-  if (goog.dom.NodeType.COMMENT == element.nodeType) {
-    return null;
-  }
-
   attributeName = attributeName.toLowerCase();
 
   // The style attribute should be a css text string that includes only what
@@ -360,58 +267,27 @@
     return bot.dom.standardizeStyleAttribute_(element.style.cssText);
   }
 
+  // In IE doc mode < 8, the "value" attribute of an <input> is only accessible
+  // as a property.
+  if (bot.userAgent.IE_DOC_PRE8 && attributeName == 'value' &&
+      bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
+    return element['value'];
+  }
+
+  // In IE < 9, element.getAttributeNode will return null for some boolean
+  // attributes that are present, such as the selected attribute on <option>
+  // elements. This if-statement is sufficient if these cases are restricted
+  // to boolean attributes whose reflected property names are all lowercase
+  // (as attributeName is by this point), like "selected". We have not
+  // found a boolean attribute for which this does not work.
+  if (bot.userAgent.IE_DOC_PRE9 && element[attributeName] === true) {
+    return String(element.getAttribute(attributeName));
+  }
+
+  // When the attribute is not present, either attr will be null or
+  // attr.specified will be false.
   var attr = element.getAttributeNode(attributeName);
-
-  // IE8/9 in standards mode handles boolean attributes differently (of
-  // course!). This if-statement is nested so the compiler can easily strip it
-  // out when compiled for non-IE browsers.
-  if (goog.userAgent.IE) {
-    if (!attr && goog.userAgent.isVersion(8) &&
-        goog.array.contains(bot.dom.BOOLEAN_ATTRIBUTES_, attributeName)) {
-      attr = element[attributeName];
-    }
-  }
-
-  if (!attr) {
-    return null;
-  }
-
-  // Attempt to always return either true or null for boolean attributes.
-  // In IE, attributes will sometimes be present even when not user-specified.
-  // We would like to rely on the 'specified' property of attribute nodes, but
-  // that is sometimes false for user-specified boolean attributes.
-  // IE does consistently yield 'true' or 'false' strings for boolean attribute
-  // values, and so we know 'false' attribute values were not user-specified.
-  if (goog.array.contains(bot.dom.BOOLEAN_ATTRIBUTES_, attributeName)) {
-    return bot.userAgent.IE_DOC_PRE9 && attr.value == 'false' ? null : 'true';
-  }
-
-  // For non-boolean attributes, we compensate for IE's extra attributes by
-  // returning null if the 'specified' property of the attributes node is false.
-  return attr.specified ? attr.value : null;
-};
-
-
-/**
- * Check if the DOM element has a particular attribute.
- * Convenience method since IE6/7 do not supply it.
- *
- * @param {!Element} element The element to use.
- * @param {string} attributeName The name of the attribute.
- * @return {boolean} Whether the node has the attribute, regardless of whether
- *      it is the default value or user defined.
- */
-bot.dom.hasAttribute = function(element, attributeName) {
-  attributeName = attributeName.toLowerCase();
-  if (element.hasAttribute) {
-    return element.hasAttribute(attributeName);
-  } else {
-    try {
-      return element.attributes[attributeName].specified;
-    } catch (e) {
-      return false;
-    }
-  }
+  return (attr && attr.specified) ? attr.value : null;
 };
 
 
@@ -806,6 +682,12 @@
     if (size.height > 0 && size.width > 0) {
       return true;
     }
+    // A vertical or horizontal SVG Path element will report zero width or
+    // height but is "shown" if it has a positive stroke-width.
+    if (bot.dom.isElement(e, 'PATH') && (size.height > 0 || size.width > 0)) {
+      var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');
+      return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
+    }
     // Zero-sized elements should still be considered to have positive size
     // if they have a child element or text node with positive size.
     return goog.array.some(e.childNodes, function(n) {
@@ -822,8 +704,8 @@
   // size of the parent
   function isOverflowHiding(e) {
     var parent = goog.style.getOffsetParent(e);
-    var parentNode = goog.userAgent.GECKO || goog.userAgent.IE || goog.userAgent.OPERA ?
-        bot.dom.getParentElement(e) : parent;
+    var parentNode = goog.userAgent.GECKO || goog.userAgent.IE ||
+        goog.userAgent.OPERA ? bot.dom.getParentElement(e) : parent;
 
     // Gecko will skip the BODY tag when calling getOffsetParent. However, the
     // combination of the overflow values on the BODY _and_ HTML tags determine
@@ -1081,7 +963,7 @@
  */
 bot.dom.getOpacity = function(elem) {
   // TODO(bsilverberg): Does this need to deal with rgba colors?
-  if (!goog.userAgent.IE) {
+  if (!bot.userAgent.IE_DOC_PRE10) {
     return bot.dom.getOpacityNonIE_(elem);
   } else {
     if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') {
diff --git a/javascript/atoms/events.js b/javascript/atoms/events.js
index 763fb49..a1911a5 100644
--- a/javascript/atoms/events.js
+++ b/javascript/atoms/events.js
@@ -22,15 +22,19 @@
 goog.provide('bot.events.EventArgs');
 goog.provide('bot.events.EventType');
 goog.provide('bot.events.KeyboardArgs');
+goog.provide('bot.events.MSGestureArgs');
+goog.provide('bot.events.MSPointerArgs');
 goog.provide('bot.events.MouseArgs');
 goog.provide('bot.events.Touch');
 goog.provide('bot.events.TouchArgs');
 
+goog.require('bot');
 goog.require('bot.Error');
 goog.require('bot.ErrorCode');
 goog.require('bot.userAgent');
 goog.require('goog.array');
 goog.require('goog.dom');
+goog.require('goog.style');
 goog.require('goog.userAgent');
 goog.require('goog.userAgent.product');
 
@@ -42,9 +46,10 @@
  * @type {boolean}
  */
 bot.events.SUPPORTS_TOUCH_EVENTS = !(goog.userAgent.IE &&
-                                   !bot.userAgent.isEngineVersion(10)) &&
+                                     !bot.userAgent.isEngineVersion(10)) &&
                                    !goog.userAgent.OPERA;
 
+
 /**
  * Whether the browser supports a native touch api.
  *
@@ -62,9 +67,20 @@
 
 
 /**
+ * Whether the browser supports the construction of MSPointer events.
+ *
+ * @const
+ * @type {boolean}
+ */
+bot.events.SUPPORTS_MSPOINTER_EVENTS =
+    goog.userAgent.IE && bot.getWindow().navigator.msPointerEnabled;
+
+
+/**
  * Arguments to initialize an event.
  *
- * @typedef {bot.events.MouseArgs|bot.events.KeyboardArgs|bot.events.TouchArgs}
+ * @typedef {bot.events.MouseArgs|bot.events.KeyboardArgs|bot.events.TouchArgs|
+             bot.events.MSGestureArgs|bot.events.MSPointerArgs}
  */
 bot.events.EventArgs;
 
@@ -128,6 +144,49 @@
 bot.events.Touch;
 
 
+/**
+ * Arguments to initialize an MSGesture event.
+ *
+ * @typedef {{clientX: number,
+ *            clientY: number,
+ *            translationX: number,
+ *            translationY: number,
+ *            scale: number,
+ *            expansion: number,
+ *            rotation: number,
+ *            velocityX: number,
+ *            velocityY: number,
+ *            velocityExpansion: number,
+ *            velocityAngular: number,
+ *            relatedTarget: Element}}
+ */
+bot.events.MSGestureArgs;
+
+
+/**
+ * Arguments to initialize an MSPointer event.
+ *
+ * @typedef {{clientX: number,
+ *            clientY: number,
+ *            button: number,
+ *            altKey: boolean,
+ *            ctrlKey: boolean,
+ *            shiftKey: boolean,
+ *            metaKey: boolean,
+ *            relatedTarget: Element,
+ *            width: number,
+ *            height: number,
+ *            pressure: number,
+ *            rotation: number,
+ *            pointerId: number,
+ *            tiltX: number,
+ *            tiltY: number,
+ *            pointerType: number,
+ *            isPrimary: boolean}}
+ */
+bot.events.MSPointerArgs;
+
+
 
 /**
  * Factory for event objects of a specific type.
@@ -210,7 +269,7 @@
 
 
 /**
- * @inheritDoc
+ * @override
  */
 bot.events.MouseEventFactory_.prototype.create = function(target, opt_args) {
   // Only Gecko supports the mouse pixel scroll event.
@@ -308,31 +367,27 @@
         args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button,
         args.relatedTarget);
 
-    // Lifted from jquery-ui tests/jquery.simulate.js
-    // IE 9+ creates events with pageX and pageY set to 0.
     // Trying to modify the properties throws an error,
     // so we define getters to return the correct values.
     if (goog.userAgent.IE &&
         event.pageX === 0 && event.pageY === 0 && Object.defineProperty) {
-      var doc = bot.getDocument().documentElement;
-      var body = bot.getDocument().body;
+      var scrollElem = goog.dom.getDomHelper(target).getDocumentScrollElement();
+      var clientElem = goog.style.getClientViewportElement(target);
+      var pageX = args.clientX + scrollElem.scrollLeft - clientElem.clientLeft;
+      var pageY = args.clientY + scrollElem.scrollTop - clientElem.clientTop;
 
-      Object.defineProperty( event, "pageX", {
+      Object.defineProperty(event, 'pageX', {
         get: function() {
-          return args.clientX +
-              ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
-              ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+          return pageX;
         }
       });
-      Object.defineProperty( event, "pageY", {
+      Object.defineProperty(event, 'pageY', {
         get: function() {
-          return args.clientY +
-              ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
-              ( doc && doc.clientTop || body && body.clientTop || 0 );
-          }
+          return pageY;
+        }
       });
     }
-}
+  }
 
   return event;
 };
@@ -356,7 +411,7 @@
 
 
 /**
- * @inheritDoc
+ * @override
  */
 bot.events.KeyboardEventFactory_.prototype.create = function(target, opt_args) {
   var args = (/** @type {!bot.events.KeyboardArgs} */ opt_args);
@@ -414,7 +469,7 @@
 
 
 /**
- * @inheritDoc
+ * @override
  */
 bot.events.TouchEventFactory_.prototype.create = function(target, opt_args) {
   if (!bot.events.SUPPORTS_TOUCH_EVENTS) {
@@ -504,6 +559,99 @@
 };
 
 
+
+/**
+ * Factory for MSGesture event objects of a specific type.
+ *
+ * @constructor
+ * @param {string} type Type of the created events.
+ * @param {boolean} bubbles Whether the created events bubble.
+ * @param {boolean} cancelable Whether the created events are cancelable.
+ * @extends {bot.events.EventFactory_}
+ * @private
+ */
+bot.events.MSGestureEventFactory_ = function(type, bubbles, cancelable) {
+  goog.base(this, type, bubbles, cancelable);
+};
+goog.inherits(bot.events.MSGestureEventFactory_, bot.events.EventFactory_);
+
+
+/**
+ * @override
+ */
+bot.events.MSGestureEventFactory_.prototype.create = function(target,
+                                                              opt_args) {
+  if (!bot.events.SUPPORTS_MSPOINTER_EVENTS) {
+    throw new bot.Error(bot.ErrorCode.UNSUPPORTED_OPERATION,
+        'Browser does not support MSGesture events.');
+  }
+
+  var args = (/** @type {!bot.events.MSGestureArgs} */ opt_args);
+  var doc = goog.dom.getOwnerDocument(target);
+  var view = goog.dom.getWindow(doc);
+  var event = doc.createEvent('MSGestureEvent');
+  var timestamp = (new Date).getTime();
+
+  // See http://msdn.microsoft.com/en-us/library/windows/apps/hh441187.aspx
+  event.initGestureEvent(this.type_, this.bubbles_, this.cancelable_, view,
+                         /*detail*/ 1, /*screenX*/ 0, /*screenY*/ 0,
+                         args.clientX, args.clientY, /*offsetX*/ 0,
+                         /*offsetY*/ 0, args.translationX, args.translationY,
+                         args.scale, args.expansion, args.rotation,
+                         args.velocityX, args.velocityY, args.velocityExpansion,
+                         args.velocityAngular, timestamp, args.relatedTarget);
+  return event;
+};
+
+
+
+/**
+ * Factory for MSPointer event objects of a specific type.
+ *
+ * @constructor
+ * @param {string} type Type of the created events.
+ * @param {boolean} bubbles Whether the created events bubble.
+ * @param {boolean} cancelable Whether the created events are cancelable.
+ * @extends {bot.events.EventFactory_}
+ * @private
+ */
+bot.events.MSPointerEventFactory_ = function(type, bubbles, cancelable) {
+  goog.base(this, type, bubbles, cancelable);
+};
+goog.inherits(bot.events.MSPointerEventFactory_, bot.events.EventFactory_);
+
+
+/**
+ * @override
+ * @suppress {checkTypes} Closure compiler externs don't know about pointer
+ *     events
+ */
+bot.events.MSPointerEventFactory_.prototype.create = function(target,
+                                                              opt_args) {
+  if (!bot.events.SUPPORTS_MSPOINTER_EVENTS) {
+    throw new bot.Error(bot.ErrorCode.UNSUPPORTED_OPERATION,
+        'Browser does not support MSPointer events.');
+  }
+
+  var args = (/** @type {!bot.events.MSPointerArgs} */ opt_args);
+  var doc = goog.dom.getOwnerDocument(target);
+  var view = goog.dom.getWindow(doc);
+  var event = doc.createEvent('MSPointerEvent');
+
+  // See http://msdn.microsoft.com/en-us/library/ie/hh772109(v=vs.85).aspx
+  event.initPointerEvent(this.type_, this.bubbles_, this.cancelable_, view,
+                         /*detail*/ 0, /*screenX*/ 0, /*screenY*/ 0,
+                         args.clientX, args.clientY, args.ctrlKey, args.altKey,
+                         args.shiftKey, args.metaKey, args.button,
+                         args.relatedTarget, /*offsetX*/ 0, /*offsetY*/ 0,
+                         args.width, args.height, args.pressure, args.rotation,
+                         args.tiltX, args.tiltY, args.pointerId,
+                         args.pointerType, /*hwTimeStamp*/ 0, args.isPrimary);
+
+  return event;
+};
+
+
 /**
  * The types of events this modules supports firing.
  *
@@ -546,7 +694,33 @@
   // Touch events.
   TOUCHEND: new bot.events.TouchEventFactory_('touchend', true, true),
   TOUCHMOVE: new bot.events.TouchEventFactory_('touchmove', true, true),
-  TOUCHSTART: new bot.events.TouchEventFactory_('touchstart', true, true)
+  TOUCHSTART: new bot.events.TouchEventFactory_('touchstart', true, true),
+
+  // MSGesture events
+  MSGESTURECHANGE: new bot.events.MSGestureEventFactory_(
+      'MSGestureChange', true, true),
+  MSGESTUREEND: new bot.events.MSGestureEventFactory_(
+      'MSGestureEnd', true, true),
+  MSGESTUREHOLD: new bot.events.MSGestureEventFactory_(
+      'MSGestureHold', true, true),
+  MSGESTURESTART: new bot.events.MSGestureEventFactory_(
+      'MSGestureStart', true, true),
+  MSGESTURETAP: new bot.events.MSGestureEventFactory_(
+      'MSGestureTap', true, true),
+  MSINERTIASTART: new bot.events.MSGestureEventFactory_(
+      'MSInertiaStart', true, true),
+
+  // MSPointer events
+  MSPOINTERDOWN: new bot.events.MSPointerEventFactory_(
+      'MSPointerDown', true, true),
+  MSPOINTERMOVE: new bot.events.MSPointerEventFactory_(
+      'MSPointerMove', true, true),
+  MSPOINTEROVER: new bot.events.MSPointerEventFactory_(
+      'MSPointerOver', true, true),
+  MSPOINTEROUT: new bot.events.MSPointerEventFactory_(
+      'MSPointerOut', true, true),
+  MSPOINTERUP: new bot.events.MSPointerEventFactory_(
+      'MSPointerUp', true, true)
 };
 
 
@@ -559,7 +733,7 @@
  * @return {boolean} Whether the event fired successfully or was cancelled.
  */
 bot.events.fire = function(target, type, opt_args) {
-  var factory = /** @type {!bot.events.EventFactory_} */ type;
+  var factory = /** @type {!bot.events.EventFactory_} */ (type);
   var event = factory.create(target, opt_args);
 
   // Ensure the event's isTrusted property is set to false, so that
diff --git a/javascript/atoms/keyboard.js b/javascript/atoms/keyboard.js
index 80cade4..d0e7e91 100644
--- a/javascript/atoms/keyboard.js
+++ b/javascript/atoms/keyboard.js
@@ -30,7 +30,6 @@
 goog.require('goog.array');
 goog.require('goog.dom.TagName');
 goog.require('goog.dom.selection');
-goog.require('goog.events.KeyCodes');
 goog.require('goog.structs.Map');
 goog.require('goog.structs.Set');
 goog.require('goog.userAgent');
@@ -276,10 +275,10 @@
   // Punctuation keys
   EQUALS: bot.Keyboard.newKey_(
       {gecko: 107, ieWebkit: 187, opera: 61}, '=', '+'),
+  SEPARATOR: bot.Keyboard.newKey_(108, ','),
   HYPHEN: bot.Keyboard.newKey_(
       {gecko: 109, ieWebkit: 189, opera: 109}, '-', '_'),
   COMMA: bot.Keyboard.newKey_(188, ',', '<'),
-  SEPARATOR: bot.Keyboard.newKey_(188, ','),
   PERIOD: bot.Keyboard.newKey_(190, '.', '>'),
   SLASH: bot.Keyboard.newKey_(191, '/', '?'),
   BACKTICK: bot.Keyboard.newKey_(192, '`', '~'),
@@ -382,8 +381,8 @@
  */
 bot.Keyboard.prototype.setKeyPressed_ = function(key, isPressed) {
   if (goog.array.contains(bot.Keyboard.MODIFIERS, key)) {
-    var modifier = /** @type {bot.Device.Modifier}*/
-        bot.Keyboard.KEY_TO_MODIFIER_.get(key.code);
+    var modifier = /** @type {bot.Device.Modifier}*/ (
+        bot.Keyboard.KEY_TO_MODIFIER_.get(key.code));
     this.modifiersState.setPressed(modifier, isPressed);
   }
 
@@ -882,5 +881,5 @@
  * @return {bot.Device.ModifiersState} Modifiers state.
  */
 bot.Keyboard.prototype.getModifiersState = function() {
-  return this.modifiersState
+  return this.modifiersState;
 };
diff --git a/javascript/atoms/locators/css.js b/javascript/atoms/locators/css.js
index d83e673..c16757e 100644
--- a/javascript/atoms/locators/css.js
+++ b/javascript/atoms/locators/css.js
@@ -18,10 +18,7 @@
 goog.provide('bot.locators.css');
 
 goog.require('bot.userAgent');
-goog.require('goog.array');
-goog.require('goog.dom');
 goog.require('goog.dom.NodeType');
-goog.require('goog.object');
 goog.require('goog.string');
 goog.require('goog.userAgent');
 
diff --git a/javascript/atoms/locators/xpath.js b/javascript/atoms/locators/xpath.js
index 88b77f4..9525ba5 100644
--- a/javascript/atoms/locators/xpath.js
+++ b/javascript/atoms/locators/xpath.js
@@ -36,13 +36,12 @@
 goog.require('bot');
 goog.require('bot.Error');
 goog.require('bot.ErrorCode');
-goog.require('bot.userAgent');
 goog.require('goog.array');
 goog.require('goog.dom');
-goog.require('goog.dom.NodeType');

-goog.require('goog.userAgent');

-goog.require('wgxpath');
+goog.require('goog.dom.NodeType');
+goog.require('goog.userAgent');
 goog.require('goog.userAgent.product');
+goog.require('wgxpath');
 
 
 /**
@@ -90,47 +89,25 @@
  * @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathEvaluator-evaluate
  */
 bot.locators.xpath.evaluate_ = function(node, path, resultType) {
-  var doc = goog.dom.getOwnerDocument(node);

-  if (goog.userAgent.IE) {

+  var doc = goog.dom.getOwnerDocument(node);
+
+  // Let the wgxpath library be compiled away unless we are on IE or Android.
+  // TODO(gdennis): Restrict this to just IE when we drop support for Froyo.
+  if (goog.userAgent.IE || goog.userAgent.product.ANDROID) {
     wgxpath.install(goog.dom.getWindow(doc));
-  } else {
-    try {
-      if (!doc.implementation ||
-          !doc.implementation.hasFeature('XPath', '3.0')) {
-        return null;
-      }
-    } catch (ex) {
-      // If the document isn't ready yet, Firefox may throw NS_ERROR_UNEXPECTED on
-      // accessing doc.implementation
-      return null;
-    }
   }
+
   try {
-    // On Android 2.2 and earlier, the evaluate function is only defined on the
-    // top-level document.
-    var docForEval;
-    if (goog.userAgent.product.ANDROID &&
-        !bot.userAgent.isProductVersion(2.3)) {
-      try {
-        docForEval = goog.dom.getWindow(doc).top.document;
-      } catch (e) {
-        // Crossed domains trying to find the evaluate function.
-        // Return null to indicate the element could not be found.
-        return null;
-      }
-    } else {
-      docForEval = doc;
-    }
     var resolver = doc.createNSResolver ?
-        doc.createNSResolver(doc.documentElement) :

-        bot.locators.xpath.DEFAULT_RESOLVER_;

-    if (goog.userAgent.IE && !goog.userAgent.isVersion(7)) {

-      // IE6, and only IE6, has an issue where calling a custom function

+        doc.createNSResolver(doc.documentElement) :
+        bot.locators.xpath.DEFAULT_RESOLVER_;
+    if (goog.userAgent.IE && !goog.userAgent.isVersion(7)) {
+      // IE6, and only IE6, has an issue where calling a custom function
       // directly attached to the document object does not correctly propagate
       // thrown errors. So in that case *only* we will use apply().
-      return docForEval.evaluate.apply(null, [path, node, resolver, resultType, null]);
+      return doc.evaluate.call(doc, path, node, resolver, resultType, null);
     } else {
-      return docForEval.evaluate(path, node, resolver, resultType, null);
+      return doc.evaluate(path, node, resolver, resultType, null);
     }
   } catch (ex) {
     // The Firefox XPath evaluator can throw an exception if the document is
@@ -169,7 +146,7 @@
  */
 bot.locators.xpath.single = function(target, root) {
 
-  function selectSingleNode() {

+  function selectSingleNode() {
     var result = bot.locators.xpath.evaluate_(root, target,
         bot.locators.XPathResult_.FIRST_ORDERED_NODE_TYPE);
     if (result) {
diff --git a/javascript/atoms/mouse.js b/javascript/atoms/mouse.js
index ee99227..7b50534 100644
--- a/javascript/atoms/mouse.js
+++ b/javascript/atoms/mouse.js
@@ -20,6 +20,7 @@
 
 goog.provide('bot.Mouse');
 goog.provide('bot.Mouse.Button');
+goog.provide('bot.Mouse.State');
 
 goog.require('bot');
 goog.require('bot.Device');
@@ -96,7 +97,7 @@
     this.hasEverInteracted_ = opt_state.hasEverInteracted;
 
     try {
-      if(bot.dom.isElement(opt_state.element)) {
+      if (bot.dom.isElement(opt_state.element)) {
         this.setElement((/** @type {!Element} */opt_state.element));
       }
     } catch (ignored) {
@@ -178,6 +179,18 @@
     buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 0, 0, 0];
   }
 
+  if (bot.userAgent.IE_DOC_10) {
+    buttonValueMap[bot.events.EventType.MSPOINTERDOWN] =
+        buttonValueMap[bot.events.EventType.MOUSEUP];
+    buttonValueMap[bot.events.EventType.MSPOINTERUP] =
+        buttonValueMap[bot.events.EventType.MOUSEUP];
+    buttonValueMap[bot.events.EventType.MSPOINTERMOVE] = [-1, -1, -1, -1];
+    buttonValueMap[bot.events.EventType.MSPOINTEROUT] =
+        buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
+    buttonValueMap[bot.events.EventType.MSPOINTEROVER] =
+        buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
+  }
+
   buttonValueMap[bot.events.EventType.DBLCLICK] =
       buttonValueMap[bot.events.EventType.CLICK];
   buttonValueMap[bot.events.EventType.MOUSEDOWN] =
@@ -189,6 +202,20 @@
 
 
 /**
+ * Maps mouse events to corresponding MSPointer event.
+ * @type {!Object.<bot.events.EventType, bot.events.EventType>}
+ * @private
+ */
+bot.Mouse.MOUSE_EVENT_MAP_ = {
+  mousedown: bot.events.EventType.MSPOINTERDOWN,
+  mousemove: bot.events.EventType.MSPOINTERMOVE,
+  mouseout: bot.events.EventType.MSPOINTEROUT,
+  mouseover: bot.events.EventType.MSPOINTEROVER,
+  mouseup: bot.events.EventType.MSPOINTERUP
+};
+
+
+/**
  * Attempts to fire a mousedown event and then returns whether or not the
  * element should receive focus as a result of the mousedown.
  *
@@ -295,6 +322,10 @@
  * @param {!goog.math.Coordinate} coords Mouse position related to the target.
  */
 bot.Mouse.prototype.move = function(element, coords) {
+  // If the element is interactable at the start of the move, it receives the
+  // full event sequence, even if hidden by an element mid sequence.
+  var toElemWasInteractable = bot.dom.isInteractable(element);
+
   var pos = goog.style.getClientPosition(element);
   this.clientXY_.x = coords.x + pos.x;
   this.clientXY_.y = coords.y + pos.y;
@@ -331,15 +362,18 @@
 
     // All browsers except IE fire the mouseover before the mousemove.
     if (!goog.userAgent.IE) {
-      this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement);
+      this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,
+          toElemWasInteractable);
     }
   }
 
-  this.fireMouseEvent_(bot.events.EventType.MOUSEMOVE);
+  this.fireMouseEvent_(bot.events.EventType.MOUSEMOVE, null, null,
+      toElemWasInteractable);
 
   // IE fires the mouseover event after the mousemove.
   if (goog.userAgent.IE && element != fromElement) {
-    this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement);
+    this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,
+        toElemWasInteractable);
   }
 
   this.nextClickIsDoubleClick_ = false;
@@ -380,15 +414,30 @@
  *
  * @param {bot.events.EventType} type Event type.
  * @param {Element=} opt_related The related element of this event.
- * @param {number=} opt_wheelDelta The wheel delta value for the event.
+ * @param {?number=} opt_wheelDelta The wheel delta value for the event.
+ * @param {boolean=} opt_force Whether the event should be fired even if the
+ *     element is not interactable.
  * @return {boolean} Whether the event fired successfully or was cancelled.
  * @private
  */
 bot.Mouse.prototype.fireMouseEvent_ = function(type, opt_related,
-                                               opt_wheelDelta) {
+                                               opt_wheelDelta, opt_force) {
   this.hasEverInteracted_ = true;
+  if (bot.userAgent.IE_DOC_10) {
+    var msPointerEvent = bot.Mouse.MOUSE_EVENT_MAP_[type];
+    if (msPointerEvent) {
+      // The pointerId for mouse events is always 1 and the mouse event is never
+      // fired if the MSPointer event fails.
+      if (!this.fireMSPointerEvent(msPointerEvent, this.clientXY_,
+          this.getButtonValue_(msPointerEvent),  /* pointerId */ 1,
+          MSPointerEvent.MSPOINTER_TYPE_MOUSE, /* isPrimary */ true,
+          opt_related, opt_force)) {
+        return false;
+      }
+    }
+  }
   return this.fireMouseEvent(type, this.clientXY_,
-      this.getButtonValue_(type), opt_related, opt_wheelDelta);
+      this.getButtonValue_(type), opt_related, opt_wheelDelta, opt_force);
 };
 
 
@@ -416,11 +465,12 @@
   return buttonValue;
 };
 
+
 /**
  * Serialize the current state of the mouse.
  * @return {!bot.Mouse.State} The current mouse state.
  */
-bot.Mouse.prototype.getState = function () {
+bot.Mouse.prototype.getState = function() {
   var state = {};
   state.buttonPressed = this.buttonPressed_;
   state.elementPressed = this.elementPressed_;
diff --git a/javascript/atoms/test/attribute_test.html b/javascript/atoms/test/attribute_test.html
index 50cd9e6..aa94036 100644
--- a/javascript/atoms/test/attribute_test.html
+++ b/javascript/atoms/test/attribute_test.html
@@ -25,91 +25,90 @@
     goog.require('goog.events.EventType');
     goog.require('goog.testing.jsunit');
   </script>
-
   <script type="text/javascript">
     function testCanFindNamedAttributes() {
       var e = bot.locators.findElement({id: 'cheddar'});
-
-      assertEquals('cheese1', bot.dom.getAttribute(e, 'name'));
+      assertAttributeEquals('cheese', e, 'name');
     }
 
     function testCanFindAttributesOnTheExpando() {
       var e = bot.locators.findElement({id: 'cheddar'});
-
-      assertEquals('lovely', bot.dom.getAttribute(e, 'unknown'));
-    }
-
-    function testReturnsNullWhenSelectedAttributeCannotBeSet() {
-      var e = bot.locators.findElement({id: 'cheddar'});
-
-      assertEquals(null, bot.dom.getAttribute(e, 'selected'));
+      assertAttributeEquals('lovely', e, 'unknown');
     }
 
     function testShouldReturnClassAttribute() {
-      var e = bot.locators.findElement({id: 'brie'});
-
-      assertEquals('tasty', bot.dom.getAttribute(e, 'class'));
+      var e = bot.locators.findElement({id: 'cheddar'});
+      assertAttributeEquals('tasty', e, 'class');
     }
 
     function testShouldReturnNullForMissingAttributes() {
-      var e = bot.locators.findElement({id: 'gouda'});
-
-      assertNull(bot.dom.getAttribute(e, 'never_there'));
-      assertNull(bot.dom.getAttribute(e, 'class'));
+      var e = bot.locators.findElement({id: 'checky'});
+      assertAttributeEquals(null, e, 'never_there');
+      assertAttributeEquals(null, e, 'class');
     }
 
     function testShouldReturnAnEmptyStringWhenAttributesValueIsAnEmptyString() {
-      var e = bot.locators.findElement({id: 'gouda'});
-
-      assertEquals('', bot.dom.getAttribute(e, 'empty'));
+      var e = bot.locators.findElement({id: 'cheddar'});
+      assertAttributeEquals('', e, 'empty');
     }
 
-    function testReturnsTrueWhenBooleanAttributesPresent() {
-      var e1 = bot.locators.findElement({id: 'peas'});
-      var e2 = bot.locators.findElement({name: 'checky'});
-      var e3 = bot.locators.findElement({id: 'checked'});
-
-      assertReturnsTrue(e1, 'selected');
-      assertReturnsTrue(e2, 'checked');
-      assertReturnsTrue(e2, 'readonly');
-      assertReturnsTrue(e3, 'checked');
-
-      function assertReturnsTrue(element, attribute) {
-        assertEquals('Should return "true" for attribute "' + attribute + '"',
-            'true', bot.dom.getAttribute(element, attribute));
+    function testReturnAttributeStringForPresentBooleanAttributes() {
+      function assertPresentBooleanAttr(elem, attr) {
+        var value = bot.dom.getAttribute(elem, attr);
+        assertEquals(String(elem.getAttribute(attr)), value);
       }
+
+      var e = bot.locators.findElement({id: 'selecty'});
+      assertPresentBooleanAttr(e, 'selected');
+
+      var e = bot.locators.findElement({id: 'checky'});
+      assertPresentBooleanAttr(e, 'disabled');
+      assertPresentBooleanAttr(e, 'readonly');
+      assertPresentBooleanAttr(e, 'checked');
+    }
+
+    function testReturnNullForAbsentBooleanAttributes() {
+      var e = bot.locators.findElement({id: 'unselecty'});
+      assertAttributeEquals(null, e, 'selected');
+
+      var e = bot.locators.findElement({id: 'unchecky'});
+      assertAttributeEquals(null, e, 'disabled');
+      assertAttributeEquals(null, e, 'readonly');
+      assertAttributeEquals(null, e, 'checked');
+    }
+
+    function testCanGetValueAttributeFromInput() {
+      var input = bot.locators.findElement({id: 'unchecky'});
+      var option = bot.locators.findElement({id: 'unselecty'});
+
+      assertAttributeEquals('unchecky', input, 'value');
+      assertAttributeEquals('unselecty', option, 'value');
     }
 
     function testAttributeMatchesAreCaseInsensitive() {
-      var e = bot.locators.findElement({name: 'checky'});
+      var e = bot.locators.findElement({id: 'checky'});
+      assertAttributeEquals(bot.dom.getAttribute(e, 'readonly'), e, 'readOnly');
+      assertAttributeEquals(bot.dom.getAttribute(e, 'name'), e, 'NaMe');
+    }
 
-      assertEquals(bot.dom.getAttribute(e, 'readonly'),
-                   bot.dom.getAttribute(e, 'readOnly'));
-      assertEquals(bot.dom.getAttribute(e, 'name'),
-                   bot.dom.getAttribute(e, 'NaMe'));
+    function assertAttributeEquals(expected, elem, attr) {
+      assertEquals('Expected attribute "' + attr + '" to equal "' +
+          expected + '"', expected, bot.dom.getAttribute(elem, attr));
     }
 
   </script>
 </head>
 <body>
-  <div name="cheese1" id="cheddar" unknown="lovely">Cheddar</div>
-  <div name="cheese2" id="brie" class="tasty">Brie</div>
-  <div name="cheese3" id="gouda" empty="">Gouda</div>
+  <div id="cheddar" name="cheese" class="tasty" unknown="lovely" empty="">Cheddar</div>
 
-  <form action="#" method="get" name="myform">
-    <select name="fish_supper">
-      <option value="fish">Fish</option>
-      <option id="chips" value="chips">Chips</option>
-      <option id="peas"  value="peas" selected>Mushy Peas</option>
-      <option value="gravy">Gravy</option>
+  <form>
+    <select>
+      <option id="selecty" selected>selecty</option>
+      <option id="unselecty" value="unselecty">unselecty</option>
     </select>
-
-    <!-- Setting checked="false" but getAttribute should return true. -->
-    <input name="checky" type="checkbox" checked="false" readonly="readonly"/>
-    <input name="radio" id="unchecked" type="radio" value="me">Me
-    <input name="radio" id="checked" type="radio" value="myself"
-           checked="checked">Myself
-    <input name="radio" type="radio" value="i">And I
+    <!-- Setting checked="false" but getAttribute should be non-null. -->
+    <input id="checky" type="checkbox" disabled readonly="readonly" checked="false"/>
+    <input id="unchecky" type="checkbox" value="unchecky"/>
   </form>
 </body>
 </html>
diff --git a/javascript/atoms/test/child_locator_test.html b/javascript/atoms/test/child_locator_test.html
index 1477ce3..0b3cde3 100644
--- a/javascript/atoms/test/child_locator_test.html
+++ b/javascript/atoms/test/child_locator_test.html
@@ -7,7 +7,6 @@
     goog.require('goog.dom');
     goog.require('goog.events.EventType');
     goog.require('goog.testing.jsunit');
-    goog.require('goog.userAgent');
     goog.require('bot');
     goog.require('bot.dom');
     goog.require('bot.locators');
@@ -104,9 +103,6 @@
     }
 
     function testCanFindChildElementByXpath() {
-      if (!document['evaluate'] && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
       var parent = bot.locators.findElement({id: 'parent'});
       var child = bot.locators.findElement({xpath: './/*[@id="child"]'}, parent);
 
@@ -114,9 +110,6 @@
     }
 
     function testCanFindChildElementsByXpath() {
-      if (!document['evaluate'] && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
       var parent = bot.locators.findElement({id: 'parent'});
       var children = bot.locators.findElements({xpath: './/*[@id="child"]'}, parent);
 
@@ -206,7 +199,6 @@
 
   <p name="child">Me either</p>
 
-
   <div id="link-parent-a">
     <a id="link" href="#">this is a link</a>
     <a name="fishsticks">this is a link</a>
diff --git a/javascript/atoms/test/click_link_test.html b/javascript/atoms/test/click_link_test.html
index 7197483..b6f0185 100644
--- a/javascript/atoms/test/click_link_test.html
+++ b/javascript/atoms/test/click_link_test.html
@@ -560,11 +560,14 @@
   <script>
     asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
     // Android needs a longer timeout due to emulator slowness.
-    // Opera 12 is sometimes slow to load new iframe pages.
+    // Opera 12, IE10, Firefox 10 are sometimes slow to load new iframe pages.
     if (goog.userAgent.product.ANDROID) {
       asyncTestCase.stepTimeout = 5000;
-    } else if (goog.userAgent.product.OPERA &&
-               bot.userAgent.isEngineVersion(12)) {
+    } else if ((goog.userAgent.product.OPERA &&
+               bot.userAgent.isEngineVersion(12)) ||
+               bot.userAgent.IE_DOC_10 ||
+               (goog.userAgent.GECKO &&
+               bot.userAgent.isEngineVersion(10))) {
       asyncTestCase.stepTimeout = 2000;
     }
   </script>
diff --git a/javascript/atoms/test/click_test.html b/javascript/atoms/test/click_test.html
index 0398c49..fb0f1b4 100644
--- a/javascript/atoms/test/click_test.html
+++ b/javascript/atoms/test/click_test.html
@@ -8,6 +8,7 @@
     goog.require('bot.action');
     goog.require('bot.locators');
     goog.require('bot.test');
+    goog.require('bot.userAgent');
     goog.require('goog.Uri');
     goog.require('goog.debug.DivConsole');
     goog.require('goog.debug.Logger');
@@ -223,8 +224,7 @@
     }
 
     // http://code.google.com/p/selenium/issues/detail?id=1207
-    function shouldRespondCorrectlyIfElementIsHiddenMidClickSequence(
-        actionEvents, action) {
+    function shouldRespondCorrectlyIfElementIsHiddenMidClickSequence(action) {
       if (!checkActionCompatibility(action)) {
         return;
       }
@@ -233,20 +233,20 @@
         return;
       }
 
+      var clickEvents = [
+        goog.events.EventType.MOUSEMOVE,
+        goog.events.EventType.MOUSEDOWN,
+        goog.events.EventType.FOCUS,
+        goog.events.EventType.MOUSEUP,
+        goog.events.EventType.CLICK
+      ];
+
       function runTest(hideOn, expectedEvents) {
         resetClicker();
         var events = [];
-        goog.events.listen(clicker,
-            [
-             goog.events.EventType.MOUSEOVER,
-             goog.events.EventType.MOUSEMOVE,
-             goog.events.EventType.MOUSEDOWN,
-             goog.events.EventType.FOCUS,
-             goog.events.EventType.MOUSEUP,
-             goog.events.EventType.CLICK],
-            function(e) {
-              events.push(e.type);
-            });
+        goog.events.listen(clicker, clickEvents, function(e) {
+          events.push(e.type);
+        });
         hideClickerOn(hideOn);
 
         assertTrue('Should start shown', bot.dom.isShown(clicker));
@@ -257,9 +257,9 @@
       }
 
       // Test hiding on each of the expected events in the click sequence.
-      for (var i = 0; i < actionEvents.length; i++) {
-        var hideOn = actionEvents[i];
-        var expectedEvents = goog.array.slice(actionEvents, 0, i + 1);
+      for (var i = 0; i < clickEvents.length; i++) {
+        var hideOn = clickEvents[i];
+        var expectedEvents = goog.array.slice(clickEvents, 0, i + 1);
 
         // Opera fires a focus event when the element is hidden on mousedown.
         if (goog.userAgent.OPERA && hideOn == goog.events.EventType.MOUSEDOWN) {
@@ -274,25 +274,13 @@
       }
     }
 
-    var expectedTapHiddenMidClickSequenceEvents = [
-        goog.events.EventType.MOUSEMOVE,
-        goog.events.EventType.MOUSEDOWN,
-        goog.events.EventType.FOCUS,
-        goog.events.EventType.MOUSEUP,
-        goog.events.EventType.CLICK];
-
-    var expectedClickHiddenMidClickSequenceEvents =
-        [goog.events.EventType.MOUSEOVER].
-          concat(expectedTapHiddenMidClickSequenceEvents);
-
     var testClickShouldRespondCorrectlyIfElementIsHiddenMidClickSequence =
         goog.partial(shouldRespondCorrectlyIfElementIsHiddenMidClickSequence,
-                     expectedClickHiddenMidClickSequenceEvents,
                      bot.action.click);
 
     var testTapShouldRespondCorrectlyIfElementIsHiddenMidClickSequence =
         goog.partial(shouldRespondCorrectlyIfElementIsHiddenMidClickSequence,
-                     expectedTapHiddenMidClickSequenceEvents, bot.action.tap);
+                     bot.action.tap);
 
 
     function cancelledMousedownDoesNotFocus(expectedEvents, action) {
@@ -306,7 +294,8 @@
            goog.events.EventType.MOUSEDOWN,
            goog.events.EventType.FOCUS,
            goog.events.EventType.MOUSEUP,
-           goog.events.EventType.CLICK],
+           goog.events.EventType.CLICK,
+           goog.events.EventType.MOUSEOUT],
           function(e) {
             events.push(e.type);
           });
@@ -323,6 +312,14 @@
         goog.events.EventType.MOUSEUP,
         goog.events.EventType.CLICK];
 
+    var expectedIETouchEvents = [
+        goog.events.EventType.MOUSEMOVE,
+        goog.events.EventType.MOUSEOVER,
+        goog.events.EventType.MOUSEDOWN,
+        goog.events.EventType.MOUSEUP,
+        goog.events.EventType.CLICK,
+        goog.events.EventType.MOUSEOUT];
+
     var expectedClickEvents = [
         FIRST_EVENT_OF_CLICK,
         SECOND_EVENT_OF_CLICK,
@@ -336,14 +333,16 @@
           bot.action.click);
 
     var testTapCancelledMousedownDoesNotFocus = goog.partial(
-          cancelledMousedownDoesNotFocus, expectedTapEvents, bot.action.tap);
+          cancelledMousedownDoesNotFocus,
+          bot.userAgent.IE_DOC_10 ? expectedIETouchEvents : expectedTapEvents,
+          bot.action.tap);
 
     function shouldBeAbleToClickOnElementsWithOpacityZero(action) {
       if (!checkActionCompatibility(action)) {
         return;
       }
       var clickJacker = findElement({id: 'clickJacker'});
-      if (goog.userAgent.IE) {
+      if (bot.userAgent.IE_DOC_PRE10) {
         clickJacker.style.filter = 'alpha(opacity=0)';
       } else {
         clickJacker.style.opacity = 0;
@@ -360,10 +359,9 @@
     var testTapShouldBeAbleToClickOnElementsWithOpacityZero = goog.partial(
           shouldBeAbleToClickOnElementsWithOpacityZero, bot.action.tap);
 
-
     function shouldNotBeAbleToClickOnElementWithPointerEventsNone(action) {
       if (!checkActionCompatibility(action) || goog.userAgent.IE || goog.userAgent.OPERA ||
-          (bot.userAgent.FIREFOX_EXTENSION && bot.userAgent.isProductVersion(3.6))) {
+          (goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {
         return;
       }
       var el = findElement({id: 'pointerEvents'});
@@ -377,8 +375,6 @@
     var testTapShouldNotBeAbleToClickOnElementWithPointerEventsNone = goog.partial(
         shouldNotBeAbleToClickOnElementWithPointerEventsNone, bot.action.tap);
 
-
-
     function testClickShouldNotScrollWhenElementInView() {
       if (bot.userAgent.MOBILE) {
         return;
@@ -432,7 +428,7 @@
     <script type="text/javascript">
       var clickJacker = document.getElementById('clickJacker');
       function setOpacity(opacity) {
-        if (goog.userAgent.IE) {
+        if (bot.userAgent.IE_DOC_PRE10) {
           clickJacker.style.filter = 'alpha(opacity=' + (opacity * 100) + ')';
         } else {
           clickJacker.style.opacity = opacity;
diff --git a/javascript/atoms/test/dom_test.html b/javascript/atoms/test/dom_test.html
index 5caf0c5..c553618 100644
--- a/javascript/atoms/test/dom_test.html
+++ b/javascript/atoms/test/dom_test.html
@@ -126,7 +126,7 @@
   }
 
   function testStandardizeStyleAttributeReturnsIdenticalStringWithLowercasedPropertyNames() {
-    toTest = [
+    var toTest = [
       {input: "Left: 0px; Text-align: center;",
         expected: "left: 0px; text-align: center;"},
       {input: "background-image: url('http://www.google.ca/Test.gif');",
@@ -136,7 +136,7 @@
         expected: "-ms-filter: 'progid:DXImageTransform(strength=50)," +
             " progid:DXImageTransform.(mirror=1)';"}
     ];
-    for (var i in toTest) {
+    for (var i = 0; i < toTest.length; i++) {
       assertObjectEquals(toTest[i].expected,
           bot.dom.standardizeStyleAttribute_(toTest[i].input));
     }
@@ -150,7 +150,7 @@
   }
 
   function testStandardizeStyleAttributeShouldWorkWithQuotesAndParens() {
-    toTest = [
+    var toTest = [
       {input: "key:value", expected: "key:value;"},
       {input: "key:value;", expected: "key:value;"},
       {input: "key1:value1; key2: value2",
@@ -173,7 +173,7 @@
         expected: "key1:\"double;quoted;string!\";" +
             " key2:'single;quoted;string;'; key3:it(is;in;parens);"}
     ];
-    for (var i in toTest) {
+    for (var i = 0; i < toTest.length; i++) {
       assertObjectEquals(toTest[i].expected,
           bot.dom.standardizeStyleAttribute_(toTest[i].input));
     }
diff --git a/javascript/atoms/test/events_test.html b/javascript/atoms/test/events_test.html
index 3711fc1..41da735 100644
--- a/javascript/atoms/test/events_test.html
+++ b/javascript/atoms/test/events_test.html
@@ -121,6 +121,50 @@
     });
   }
 
+  function testCanFireMSGestureEvent() {
+    if (!bot.events.SUPPORTS_MSPOINTER_EVENTS) {
+      return;
+    }
+    fireAndTest(bot.events.EventType.MSGESTURESTART, {
+      clientX: 0,
+      clientY: 0,
+      translationX: 0,
+      translationY: 0,
+      scale: 0,
+      expansion: 0,
+      rotation: 0,
+      velocityX: 0,
+      velocityY: 0,
+      velocityExpansion: 0,
+      velocityAngular: 0,
+      relatedTarget: null
+    });
+  }
+
+  function testCanFireMSPointerEvent() {
+    if (!bot.events.SUPPORTS_MSPOINTER_EVENTS) {
+      return;
+    }
+    fireAndTest(bot.events.EventType.MSPOINTERMOVE, {
+      clientX: 0,
+      clientY: 0,
+      button: 0,
+      altKey: false,
+      ctrlKey: false,
+      metaKey: false,
+      relatedTarget: null,
+      width: 1,
+      height: 1,
+      pressure: 10,
+      rotation: 0,
+      pointerIdArg: 1,
+      tiltX: 0,
+      tiltY: 0,
+      pointerType: MSPointerEvent.MSPOINTER_TYPE_MOUSE,
+      isPrimary: 2
+    });
+  }
+
   function testIsSynthentic() {
     fireAndTest(bot.events.EventType.CHANGE, {}, function(event) {
       assertTrue(bot.events.isSynthetic(event));
diff --git a/javascript/atoms/test/focus_test.html b/javascript/atoms/test/focus_test.html
index e03f09e..f7d4e07 100644
--- a/javascript/atoms/test/focus_test.html
+++ b/javascript/atoms/test/focus_test.html
@@ -61,7 +61,7 @@
     }
 
     function assertElementIsActiveElement(expected) {
-      var actual = document.activeElement;
+      var actual = goog.dom.getActiveElement(document);
       if (actual) {
         // NOTE(jleyba): Using assertEquals(expected, document.activeElement)
         // causes an error in IE when the assertion fails (from trying to
diff --git a/javascript/atoms/test/gestures_test.html b/javascript/atoms/test/gestures_test.html
index e84556b..6c66d6e 100644
--- a/javascript/atoms/test/gestures_test.html
+++ b/javascript/atoms/test/gestures_test.html
@@ -18,10 +18,10 @@
     function setUp() {
       elem = goog.dom.getElement('multitouch');
       goog.events.removeAll();
-      startCoords = null;
-      firstMoveCoords = null;
-      secondMoveCoords = null;
-      endCoords = null;
+      startCoords = [];
+      firstMoveCoords = [];
+      secondMoveCoords = [];
+      endCoords = [];
 
       function handleCoords(event, touchListName) {
         event.preventDefault();
@@ -29,20 +29,20 @@
         var touches = e[touchListName];
         assertEquals(2, touches.length);
         var touch0 = touches[0], touch1 = touches[1];
-        return {
-          coord0: new goog.math.Coordinate(touch0.clientX, touch0.clientY),
-          coord1: new goog.math.Coordinate(touch1.clientX, touch1.clientY)
-        };
+        return [
+          new goog.math.Coordinate(touch0.clientX, touch0.clientY),
+          new goog.math.Coordinate(touch1.clientX, touch1.clientY)
+        ];
       }
 
       goog.events.listen(elem, goog.events.EventType.TOUCHSTART, function(e) {
-        assert('touchstart already fired', !startCoords);
+        assert('touchstart already fired', !startCoords.length);
         startCoords = handleCoords(e, 'touches');
       });
 
       goog.events.listen(elem, goog.events.EventType.TOUCHMOVE, function(e) {
-        assert('two touchmoves already fired', !secondMoveCoords);
-        if (!firstMoveCoords) {
+        assert('two touchmoves already fired', !secondMoveCoords.length);
+        if (!firstMoveCoords.length) {
           firstMoveCoords = handleCoords(e, 'touches');
         } else {
           secondMoveCoords = handleCoords(e, 'touches');
@@ -50,12 +50,39 @@
       });
 
       goog.events.listen(elem, goog.events.EventType.TOUCHEND, function(e) {
-        assert('touchend already fired', !endCoords);
+        assert('touchend already fired', !endCoords.length);
         endCoords = handleCoords(e, 'changedTouches');
       });
+
+      function handlePointerCoords(event) {
+        event.preventDefault();
+        var e = event.getBrowserEvent();
+        return new goog.math.Coordinate(e.clientX, e.clientY);
+      }
+
+      goog.events.listen(elem, goog.events.EventType.MSPOINTERDOWN,
+                         function(e) {
+        assert('MSPointerDown already fired', startCoords.length < 2);
+        startCoords.push(handlePointerCoords(e));
+      });
+
+      goog.events.listen(elem, goog.events.EventType.MSPOINTERMOVE,
+                         function(e) {
+        assert('MSPointerMoves already fired', secondMoveCoords.length < 2);
+        if (firstMoveCoords.length < 2) {
+          firstMoveCoords.push(handlePointerCoords(e));
+        } else {
+          secondMoveCoords.push(handlePointerCoords(e));
+        }
+      });
+
+      goog.events.listen(elem, goog.events.EventType.MSPOINTERUP, function(e) {
+        assert('MSPointerUp already fired', endCoords.length < 2);
+        endCoords.push(handlePointerCoords(e));
+      });
     }
 
-    var EPSILON = 0.000000000001;
+    var EPSILON = 0.01;
 
     function assertApproxEquals(comment, expected, actual) {
       // Android 4+ coerces real numbered coordinates to their floor values.
@@ -72,25 +99,25 @@
 
     function assertMultitouchPoints(start0X, start0Y, start1X, start1Y,
         mid0X, mid0Y, mid1X, mid1Y, end0X, end0Y, end1X, end1Y) {
-      assertApproxEquals('start 0 x', start0X, startCoords.coord0.x);
-      assertApproxEquals('start 0 y', start0Y, startCoords.coord0.y);
-      assertApproxEquals('start 1 x', start1X, startCoords.coord1.x);
-      assertApproxEquals('start 1 y', start1Y, startCoords.coord1.y);
+      assertApproxEquals('start 0 x', start0X, startCoords[0].x);
+      assertApproxEquals('start 0 y', start0Y, startCoords[0].y);
+      assertApproxEquals('start 1 x', start1X, startCoords[1].x);
+      assertApproxEquals('start 1 y', start1Y, startCoords[1].y);
 
-      assertApproxEquals('first move 0 x', mid0X, firstMoveCoords.coord0.x);
-      assertApproxEquals('first move 0 y', mid0Y, firstMoveCoords.coord0.y);
-      assertApproxEquals('first move 1 x', mid1X, firstMoveCoords.coord1.x);
-      assertApproxEquals('first move 1 y', mid1Y, firstMoveCoords.coord1.y);
+      assertApproxEquals('first move 0 x', mid0X, firstMoveCoords[0].x);
+      assertApproxEquals('first move 0 y', mid0Y, firstMoveCoords[0].y);
+      assertApproxEquals('first move 1 x', mid1X, firstMoveCoords[1].x);
+      assertApproxEquals('first move 1 y', mid1Y, firstMoveCoords[1].y);
 
-      assertApproxEquals('second move 0 x', end0X, secondMoveCoords.coord0.x);
-      assertApproxEquals('second move 0 y', end0Y, secondMoveCoords.coord0.y);
-      assertApproxEquals('second move 1 x', end1X, secondMoveCoords.coord1.x);
-      assertApproxEquals('second move 1 y', end1Y, secondMoveCoords.coord1.y);
+      assertApproxEquals('second move 0 x', end0X, secondMoveCoords[0].x);
+      assertApproxEquals('second move 0 y', end0Y, secondMoveCoords[0].y);
+      assertApproxEquals('second move 1 x', end1X, secondMoveCoords[1].x);
+      assertApproxEquals('second move 1 y', end1Y, secondMoveCoords[1].y);
 
-      assertApproxEquals('end 0 x', end0X, endCoords.coord0.x);
-      assertApproxEquals('end 0 y', end0Y, endCoords.coord0.y);
-      assertApproxEquals('end 1 x', end1X, endCoords.coord1.x);
-      assertApproxEquals('end 1 y', end1Y, endCoords.coord1.y);
+      assertApproxEquals('end 0 x', end0X, endCoords[0].x);
+      assertApproxEquals('end 0 y', end0Y, endCoords[0].y);
+      assertApproxEquals('end 1 x', end1X, endCoords[1].x);
+      assertApproxEquals('end 1 y', end1Y, endCoords[1].y);
     }
 
     function testPinchInward() {
diff --git a/javascript/atoms/test/history_test.html b/javascript/atoms/test/history_test.html
index db107b5..93979ad 100644
--- a/javascript/atoms/test/history_test.html
+++ b/javascript/atoms/test/history_test.html
@@ -36,11 +36,13 @@
   // this page when the popup has loaded (b/3504107). On Opera 11.5, the popup
   // blocker is currently enabled (b/5746540). On Webview (Android),
   // window.open will cause a dialog prompt that will cause load failure.
+  // On IE 10, the popup block is enabled (b/7126679).
   // For these browsers, we test history in an iframe instead of a popup,
   // which limits the history properties we can reliably check.
   // TODO (gdennis): Use popups on all browsers once these bugs are fixed.
   if (bot.test.isSeleniumBacked() || goog.userAgent.product.ANDROID ||
-      (goog.userAgent.OPERA && bot.userAgent.isEngineVersion(11.5))) {
+      (goog.userAgent.OPERA && bot.userAgent.isEngineVersion(11.5)) ||
+      bot.userAgent.IE_DOC_10) {
     // A bug in prior versions of WebKit (persisting into Safari 5) causes the
     // initial page of an iframe to not enter the browser history, so we
     // initialize the iframe to a dummy page and the test cases here do not
diff --git a/javascript/atoms/test/html5/appcache_test.html b/javascript/atoms/test/html5/appcache_test.html
index 1834fcc..768f791 100644
--- a/javascript/atoms/test/html5/appcache_test.html
+++ b/javascript/atoms/test/html5/appcache_test.html
@@ -19,13 +19,11 @@
     // WebDriver does not enable application cache for Android-Froyo (b/5478400).
     // WebDriver does not enable application cache for Firefox (b/5787180).
     // Selenium breaks application cache on Opera (b/578165).
-    // Chrome/Linux does not always cache (b/6658688).
     var APPCACHE_NOT_WORKING =
       !bot.html5.isSupported(bot.html5.API.APPCACHE) ||
       goog.userAgent.product.ANDROID ||
       goog.userAgent.product.FIREFOX ||
-      (goog.userAgent.OPERA && bot.test.isSeleniumBacked()) ||
-      (goog.userAgent.product.CHROME && goog.userAgent.LINUX);
+      (goog.userAgent.OPERA && bot.test.isSeleniumBacked());
 
 
     function testGetStatusWithHtmlManifest() {
diff --git a/javascript/atoms/test/locator_test.html b/javascript/atoms/test/locator_test.html
index 9f31f5a..8752849 100644
--- a/javascript/atoms/test/locator_test.html
+++ b/javascript/atoms/test/locator_test.html
@@ -22,19 +22,13 @@
   <script type="text/javascript">
     goog.require('bot');
     goog.require('bot.ErrorCode');
-    goog.require('bot.locators');

-    goog.require('goog.events.EventType');

-    goog.require('goog.userAgent');
+    goog.require('bot.locators');
+    goog.require('goog.events.EventType');
     goog.require('goog.testing.PropertyReplacer');
     goog.require('goog.testing.jsunit');
   </script>
 
   <script type="text/javascript">
-    var CAN_SELECT_SINGLE_NODE =
-        document.documentElement['selectSingleNode'] || document['evaluate'];
-    var CAN_SELECT_MANY_NODES =
-        document.documentElement['selectNodes'] || document['evaluate'];
-
     function testCanFindById() {
       var e = bot.locators.findElement({id: 'x'});
 
@@ -161,10 +155,6 @@
     }
 
     function testCanLocateElementsUsingXPath() {
-      if (!CAN_SELECT_SINGLE_NODE && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }

-
       var doggies = bot.locators.findElement({xpath: "//*[@id = 'after']"});
 
       assertNotNull(doggies);
@@ -172,30 +162,17 @@
     }
 
     function testCanLocateElementsUsingXPathInIframe() {
-      if (!CAN_SELECT_SINGLE_NODE && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       var frameDoc = window.frames[0].document;
       var frameElement = bot.locators.findElement({xpath: '//body/h1'}, frameDoc);
       assertNotNull(frameElement);
       assertEquals('H1', frameElement.tagName);
     }
 
-
     function testWillReturnNullIfNoMatchUsingXPathIsFound() {
-      if (!CAN_SELECT_SINGLE_NODE && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       assertNull(bot.locators.findElement({xpath: '//fish'}));
     }
 
     function testShouldThrowInvalidSelectorErrorWhenXPathIsSyntacticallyInvalidInSingle() {
-      if (!CAN_SELECT_SINGLE_NODE && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       try {
         bot.locators.findElement({xpath: 'this][isnot][valid'});
         fail('Should not have succeeded because the xpath expression is ' +
@@ -208,10 +185,6 @@
     }
 
     function testShouldThrowInvalidSelectorErrorWhenXPathReturnsAWrongTypeInSingle() {
-      if (!CAN_SELECT_SINGLE_NODE && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }

-

       try {
         bot.locators.findElement({xpath: 'count(//fish)'});
         fail('Should not have succeeded because the xpath expression does ' +
@@ -252,20 +225,12 @@
     }
 
     function testCanFindManyElementsViaXPath() {
-      if (!CAN_SELECT_MANY_NODES && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       var bad = bot.locators.findElements({xpath: '//*[@name = "after"]'});
 
       assertEquals(2, bad.length);
     }
 
     function testShouldThrowInvalidSelectorErrorWhenXPathIsSyntacticallyInvalidInMany() {
-      if (!CAN_SELECT_MANY_NODES && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       try {
         bot.locators.findElements({xpath: 'this][isnot][valid'});
         fail('Should not have succeeded because the xpath expression is ' +
@@ -278,10 +243,6 @@
     }
 
     function testShouldThrowInvalidSelectorErrorWhenXPathReturnsAWrongTypeInMany() {
-      if (!CAN_SELECT_MANY_NODES && !goog.userAgent.IE) {
-        return;  // Skip this until we get xpath working on all browsers
-      }
-
       try {
         bot.locators.findElements({xpath: 'count(//fish)'});
         fail('Should not have succeeded because the xpath expression does ' +
@@ -440,5 +401,5 @@
   <a href="#" id="comma-in-alt" alt="has, a comma">has, a comma</a>
 
   <iframe src="./testdata/iframe_page.html"></iframe>
-  </body>
+</body>
 </html>
diff --git a/javascript/atoms/test/mouse_test.html b/javascript/atoms/test/mouse_test.html
index 69d7b9f..24bb9b6 100644
--- a/javascript/atoms/test/mouse_test.html
+++ b/javascript/atoms/test/mouse_test.html
@@ -12,8 +12,9 @@
   goog.require('goog.events');
   goog.require('goog.events.EventType');
   goog.require('goog.math.Coordinate');
-  goog.require('goog.testing.jsunit');
+  goog.require('goog.style');
   goog.require('goog.testing.AsyncTestCase');
+  goog.require('goog.testing.jsunit');
   goog.require('goog.userAgent');
 </script>
 <body>
@@ -42,7 +43,12 @@
     goog.events.EventType.MOUSEPIXELSCROLL,
     goog.events.EventType.CLICK,
     goog.events.EventType.CONTEXTMENU,
-    goog.events.EventType.DBLCLICK
+    goog.events.EventType.DBLCLICK,
+    goog.events.EventType.MSPOINTERDOWN,
+    goog.events.EventType.MSPOINTERMOVE,
+    goog.events.EventType.MSPOINTEROVER,
+    goog.events.EventType.MSPOINTEROUT,
+    goog.events.EventType.MSPOINTERUP
   ];
 
   function setUpPage() {
@@ -79,14 +85,46 @@
     events = [];
   }
 
-  function mouseoverSequence(elem, button) {
+  function mousedownEvents(elem, button) {
+     var events = [goog.events.EventType.MOUSEDOWN, elem, button];
+     return !bot.userAgent.IE_DOC_10 ? events :
+         [goog.events.EventType.MSPOINTERDOWN, elem, button].concat(events);
+  }
+
+  function mousemoveEvents(elem, button) {
+     var events = [goog.events.EventType.MOUSEMOVE, elem, button];
+     return bot.userAgent.IE_DOC_10 ?
+         [goog.events.EventType.MSPOINTERMOVE, elem, -1].concat(events) :
+         events;
+  }
+
+  function mouseoutEvents(elem, button) {
+     var events = [goog.events.EventType.MOUSEOUT, elem, button];
+     return bot.userAgent.IE_DOC_10 ?
+         [goog.events.EventType.MSPOINTEROUT, elem, -1].concat(events) :
+         events;
+  }
+
+  function mouseupEvents(elem, button) {
+     var events = [goog.events.EventType.MOUSEUP, elem, button];
+     return bot.userAgent.IE_DOC_10 ?
+         [goog.events.EventType.MSPOINTERUP, elem, button].concat(events) :
+         events;
+  }
+
+  function mouseoverAndMoveEvents(elem, button) {
+    function mouseoverEvents(elem, button) {
+       var events = [goog.events.EventType.MOUSEOVER, elem, button];
+       return bot.userAgent.IE_DOC_10 ?
+           [goog.events.EventType.MSPOINTEROVER, elem, -1].concat(events) :
+           events;
+    }
     // IE fires the movemove *before* the mouseover event, and
     // IE < 9 always supplies a mouseover button value of 0.
-    return goog.userAgent.IE ? [goog.events.EventType.MOUSEMOVE, elem, button,
-                                goog.events.EventType.MOUSEOVER, elem,
-                                bot.userAgent.IE_DOC_9 ? button : 0] :
-                               [goog.events.EventType.MOUSEOVER, elem, button,
-                                goog.events.EventType.MOUSEMOVE, elem, button];
+    return goog.userAgent.IE ?
+        mousemoveEvents(elem, button).concat(
+             mouseoverEvents(elem, bot.userAgent.IE_DOC_9 ? button : 0)) :
+        mouseoverEvents(elem, button).concat(mousemoveEvents(elem, button));
   }
 
   function testNoClickWhenPressHiddenElement() {
@@ -97,7 +135,7 @@
     mouse.pressButton(bot.Mouse.Button.LEFT);
     mouse.releaseButton();
 
-    assertEvents(mouseoverSequence(greenDiv, 0));
+    assertEvents(mouseoverAndMoveEvents(greenDiv, 0));
     goog.style.showElement(greenDiv, true);
   }
 
@@ -109,9 +147,9 @@
     mouse.releaseButton();
 
     assertEvents(
-      mouseoverSequence(redDiv, 0),
-      goog.events.EventType.MOUSEDOWN, redDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEUP, redDiv, b({ie: 1, wk: 0, ff: 0}),
+      mouseoverAndMoveEvents(redDiv, 0),
+      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
       goog.events.EventType.CLICK, redDiv, 0
     );
   }
@@ -126,11 +164,11 @@
 
     // No click if we released on another element.
     assertEvents(
-      mouseoverSequence(greenDiv, 0),
-      goog.events.EventType.MOUSEDOWN, greenDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEOUT, greenDiv, 0,
-      mouseoverSequence(redDiv, b({ie: 1, wk: 0, ff: 0})),
-      goog.events.EventType.MOUSEUP, redDiv, b({ie: 1, wk: 0, ff: 0})
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mousedownEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseoutEvents(greenDiv, 0),
+      mouseoverAndMoveEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0}))
     );
   }
 
@@ -144,11 +182,11 @@
 
     // Right click triggers contextmenu even when released over another element.
     assertEvents(
-      mouseoverSequence(greenDiv, 0),
-      goog.events.EventType.MOUSEDOWN, greenDiv, 2,
-      goog.events.EventType.MOUSEOUT, greenDiv, b({ie: 0, wk: 2, ff: 0}),
-      mouseoverSequence(redDiv, b({ie: 2, wk: 2, ff: 0})),
-      goog.events.EventType.MOUSEUP, redDiv, 2,
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mousedownEvents(greenDiv, 2),
+      mouseoutEvents(greenDiv, b({ie: 0, wk: 2, ff: 0})),
+      mouseoverAndMoveEvents(redDiv, b({ie: 2, wk: 2, ff: 0})),
+      mouseupEvents(redDiv, 2),
       goog.events.EventType.CONTEXTMENU, redDiv, b({ie: 0, wk: 2, ff: 2})
     );
   }
@@ -163,12 +201,12 @@
     mouse.releaseButton();
 
     assertEvents(
-      mouseoverSequence(redDiv, 0),
-      goog.events.EventType.MOUSEDOWN, redDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEUP, redDiv, b({ie: 1, wk: 0, ff: 0}),
+      mouseoverAndMoveEvents(redDiv, 0),
+      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
       goog.events.EventType.CLICK, redDiv, 0,
-      goog.events.EventType.MOUSEDOWN, redDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEUP, redDiv, b({ie: 1, wk: 0, ff: 0}),
+      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
       goog.events.EventType.CLICK, redDiv, 0,
       goog.events.EventType.DBLCLICK, redDiv, 0
     );
@@ -185,14 +223,14 @@
     mouse.releaseButton();
 
     assertEvents(
-      mouseoverSequence(greenDiv, 0),
-      goog.events.EventType.MOUSEDOWN, greenDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEUP, greenDiv, b({ie: 1, wk: 0, ff: 0}),
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mousedownEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
       goog.events.EventType.CLICK, greenDiv, 0,
-      goog.events.EventType.MOUSEOUT, greenDiv, 0,
-      mouseoverSequence(redDiv, 0),
-      goog.events.EventType.MOUSEDOWN, redDiv, b({ie: 1, wk: 0, ff: 0}),
-      goog.events.EventType.MOUSEUP, redDiv, b({ie: 1, wk: 0, ff: 0}),
+      mouseoutEvents(greenDiv, 0),
+      mouseoverAndMoveEvents(redDiv, 0),
+      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
+      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
       goog.events.EventType.CLICK, redDiv, 0
     );
   }
@@ -205,8 +243,8 @@
     mouse.move(greenDiv, coords2);
 
     assertEvents(
-      mouseoverSequence(greenDiv, 0),
-      goog.events.EventType.MOUSEMOVE, greenDiv, 0
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mousemoveEvents(greenDiv, 0)
     );
   }
 
@@ -217,9 +255,9 @@
     mouse.move(redDiv, coords);
 
     assertEvents(
-      mouseoverSequence(greenDiv, 0),
-      goog.events.EventType.MOUSEOUT, greenDiv, 0,
-      mouseoverSequence(redDiv, 0)
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mouseoutEvents(greenDiv, 0),
+      mouseoverAndMoveEvents(redDiv, 0)
     );
   }
 
@@ -263,7 +301,7 @@
 
   function testMoveMouseFromClosedWindowDoesNotError() {
     var mouse = new bot.Mouse();
-    var coord =  new goog.math.Coordinate(0, 0);
+    var coord = new goog.math.Coordinate(0, 0);
     var iframe = document.createElement('iframe');
 
     goog.events.listenOnce(iframe, 'load', function() {
@@ -284,6 +322,23 @@
     document.body.appendChild(iframe);
   }
 
+  function testMoveMouseFiresAllEventsOnElementHiddenMidSequence() {
+    var coords = new goog.math.Coordinate(5, 5);
+    var mouse = new bot.Mouse();
+    mouse.move(greenDiv, coords);
+    goog.events.listen(greenDiv, 'mouseout', function() {
+      goog.style.showElement(redDiv, false);
+    });
+    mouse.move(redDiv, coords);
+    goog.style.showElement(redDiv, true);
+
+    assertEvents(
+      mouseoverAndMoveEvents(greenDiv, 0),
+      mouseoutEvents(greenDiv, 0),
+      mouseoverAndMoveEvents(redDiv, 0)
+    );
+  }
+
   function testScrollMouseZeroTicksThrowsError() {
     var mouse = new bot.Mouse();
     mouse.move(greenDiv, new goog.math.Coordinate(5, 5));
@@ -299,13 +354,13 @@
 
     if (goog.userAgent.GECKO) {
       assertEvents(
-        mouseoverSequence(greenDiv, 0),
+        mouseoverAndMoveEvents(greenDiv, 0),
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
         goog.events.EventType.MOUSEPIXELSCROLL, greenDiv, 0
       );
     } else {
       assertEvents(
-        mouseoverSequence(greenDiv, 0),
+        mouseoverAndMoveEvents(greenDiv, 0),
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0
       );
     }
@@ -318,7 +373,7 @@
 
     if (goog.userAgent.GECKO) {
       assertEvents(
-        mouseoverSequence(greenDiv, 0),
+        mouseoverAndMoveEvents(greenDiv, 0),
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
         goog.events.EventType.MOUSEPIXELSCROLL, greenDiv, 0,
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
@@ -326,7 +381,7 @@
       );
     } else {
       assertEvents(
-        mouseoverSequence(greenDiv, 0),
+        mouseoverAndMoveEvents(greenDiv, 0),
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
         goog.events.EventType.MOUSEWHEEL, greenDiv, 0
       );
diff --git a/javascript/atoms/test/opacity_test.html b/javascript/atoms/test/opacity_test.html
index 28e8242..8650aa8 100644
--- a/javascript/atoms/test/opacity_test.html
+++ b/javascript/atoms/test/opacity_test.html
@@ -36,7 +36,7 @@
     function testOpacity() {
       var suite = buildTestSuites();
 
-      if(!goog.userAgent.IE) {
+      if(!bot.userAgent.IE_DOC_PRE10) {
         runTestSuite(suite, 'others');
       } else if(bot.userAgent.isEngineVersion(8)) {
         runTestSuite(suite, 'ie8');
diff --git a/javascript/atoms/test/property_test.html b/javascript/atoms/test/property_test.html
index 50634d3..5e1e8f5 100644
--- a/javascript/atoms/test/property_test.html
+++ b/javascript/atoms/test/property_test.html
@@ -50,18 +50,8 @@
       assertTrue(result);
     }
 
-    function testShouldAliasClassNameProperty() {
-      var e = bot.locators.findElement({id: 'brie'});
-
-      assertEquals('tasty', bot.dom.getProperty(e, 'class'));
-      assertEquals('tasty', bot.dom.getProperty(e, 'className'));
-    }
-
-    function testShouldHaveDefaultValueOfFalseForDisabledProperty() {
-      var working = bot.locators.findElement({id: 'not_disabled'});
+    function testShouldReturnDisabledProperty() {
       var disabled = bot.locators.findElement({id: 'is_disabled'});
-
-      assertFalse(bot.dom.getProperty(working, 'disabled'));
       assertTrue(bot.dom.getProperty(disabled, 'disabled'));
     }
 
@@ -110,6 +100,5 @@
 
     <input id="is_disabled" name="foo" disabled />
   </form>
-  <p id="not_disabled">This paragraph can check for the disabled property.</p>
 </body>
 </html>
diff --git a/javascript/atoms/test/scrolled_into_view_test.html b/javascript/atoms/test/scrolled_into_view_test.html
index f982404..fb05d0f 100644
--- a/javascript/atoms/test/scrolled_into_view_test.html
+++ b/javascript/atoms/test/scrolled_into_view_test.html
@@ -1,16 +1,14 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<!DOCTYPE html>
 <html>
 <head>
   <title>scrolling_test.html</title>
   <script src="test_bootstrap.js"></script>
   <script type="text/javascript">
-    goog.require('bot');
-    goog.require('bot.action');
     goog.require('bot.dom');
+    goog.require('bot.locators');
+    goog.require('bot.userAgent');
     goog.require('goog.events');
     goog.require('goog.testing.jsunit');
-    goog.require('goog.userAgent');
-    goog.require('goog.userAgent.product');
   </script>
 </head>
 <body>
@@ -130,6 +128,10 @@
     }
 
     function testNestedIframeWhereMiddleNeedsScrollingIsNotInView() {
+      // Android Froyo zooms out to auto-fit the page and make the element visible.
+      if (bot.userAgent.ANDROID_PRE_GINGERBREAD) {
+        return;
+      }
       var iframe = findElement({id: 'nested-iframe-parent'});
       var frameDocument = iframe.contentWindow.document;
       var innerIframe = findElement({id: 'middle'}, frameDocument);
diff --git a/javascript/atoms/test/style_test.html b/javascript/atoms/test/style_test.html
index 1005059..6fe5eb9 100644
--- a/javascript/atoms/test/style_test.html
+++ b/javascript/atoms/test/style_test.html
@@ -243,7 +243,8 @@
 
       function testEffectiveStyleReturnsQuotedShortMsFilterAsFilter() {
         var e = bot.locators.findElement({id: 'msFilterQuotedFilterShort'});
-        if (goog.userAgent.IE && bot.userAgent.isEngineVersion(8)) {
+        if (goog.userAgent.IE && bot.userAgent.isEngineVersion(8) &&
+            !bot.userAgent.isEngineVersion(10)) {
           assertEquals('alpha(opacity=0)', bot.dom.getInlineStyle(e, 'filter'));
         } else {
           // -ms-filters make sense only on IE.
@@ -253,7 +254,8 @@
 
       function testEffectiveStyleReturnsQuotedLongMsFilterAsFilter() {
         var e = bot.locators.findElement({id: 'msFilterQuotedFilterLong'});
-        if (goog.userAgent.IE && bot.userAgent.isEngineVersion(8)) {
+        if (goog.userAgent.IE && bot.userAgent.isEngineVersion(8) &&
+            !bot.userAgent.isEngineVersion(10)) {
           assertEquals('progid:DXImageTransform.Microsoft.Alpha(Opacity=20)',
               bot.dom.getInlineStyle(e, 'filter'));
         } else {
diff --git a/javascript/atoms/test/svg_test.html b/javascript/atoms/test/svg_test.html
index a37b165..92b908b 100644
--- a/javascript/atoms/test/svg_test.html
+++ b/javascript/atoms/test/svg_test.html
@@ -1,10 +1,5 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html
-      PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:svg="http://www.w3.org/2000/svg"
-      xmlns:xlink="http://www.w3.org/1999/xlink">
+<!DOCTYPE html>
+<html>
 <head>
   <title>svg_test.html</title>
   <script src="test_bootstrap.js"></script>
@@ -12,77 +7,51 @@
     goog.require('bot.action');
     goog.require('bot.dom');
     goog.require('bot.locators');
+    goog.require('bot.test');
     goog.require('goog.testing.jsunit');
     goog.require('goog.userAgent');
   </script>
   <script type="text/javascript">
-    // Skip this test on IE and Android until SVG is natively supported.
-    var SUPPORTS_SVG = !goog.userAgent.IE && !goog.userAgent.product.ANDROID;
-
     function testSvgElementsShouldBeVisible() {
-      if (!SUPPORTS_SVG) {
+      if (!bot.test.SUPPORTS_INLINE_SVG) {
         return;
       }
-      var doc = window.frames['chart'].document.documentElement;
-      var element = bot.locators.findElement({id: 'title'}, doc);
+      var element = bot.locators.findElement({id: 'rect'});
       assertTrue(bot.dom.isShown(element));
     }
 
     function testFocusingOnSvgElementDoesNotThrow() {
-      if (!SUPPORTS_SVG) {
+      if (!bot.test.SUPPORTS_INLINE_SVG) {
         return;
       }
-      var doc = window.frames['chart'].document.documentElement;
-      var element = bot.locators.findElement({id: 'title'}, doc);
-      var otherElement = bot.locators.findElement({id: 'someText'});
-
+      var element = bot.locators.findElement({id: 'rect'});
+      var otherElement = bot.locators.findElement({id: 'text'});
       bot.action.focusOnElement(otherElement);
       bot.action.focusOnElement(element);
     }
 
-    function testCanFindElementsWithinFramedSvg() {
-      if (!SUPPORTS_SVG) {
-        return;
-      }
-      var doc = window.frames['chart'].document.documentElement;
-      var element = bot.locators.findElement({tagName: 'text'}, doc);
-      assertEquals('title', element.id);
-    }
-
-    function testLocatingSvgElementsByTagNameIsCaseSensitive() {
-      if (!SUPPORTS_SVG) {
-        return;
-      }
-      // Just documenting this for posterity...
-      var doc = window.frames['chart'].document.documentElement;
-      assertNull(bot.locators.findElement({tagName: 'TEXT'}, doc));
-      assertNotNull(bot.locators.findElement({tagName: 'text'}, doc));
-    }
-
     function testGettingTextOfSvgElementDoesNotThrow() {
-      if (!SUPPORTS_SVG) {
+      if (!bot.test.SUPPORTS_INLINE_SVG) {
         return;
       }
+      var element = bot.locators.findElement({id: 'text'});
+      bot.dom.getVisibleText(element);
+    }
 
-      var doc = window.frames['chart'].document.documentElement;
-      bot.dom.getVisibleText(doc);
+    function testSvgLineWithZeroSizeBBoxIsShown() {
+      if (!bot.test.SUPPORTS_INLINE_SVG) {
+        return;
+      }
+      var element = bot.locators.findElement({id: 'path'});
+      assertTrue(bot.dom.isShown(element));
     }
   </script>
 </head>
 <body>
-  <div id="someText">
-    Some text for the chart.
-  </div>
-  <div id="result">Nothing.</div>
-  <div id="chart_container" style="width: 400px; height: 220px; border: 1px solid #808080;">
-    <script type="text/javascript">
-      if (SUPPORTS_SVG) {
-        document.write('<iframe name="chart" src="testdata/chart.svg" ' +
-                       'width="400" height="220" ' +
-                       'type="image/svg+xml"></iframe>');
-      }
-    </script>
-  </div>
-  <div style="display: none; position: absolute; top: 230px; left: 410px; white-space: nowrap; font-family: Arial; font-size: 12px;">WOrange</div>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 50">
+  <rect id="rect" fill="red" stroke="none" height="12" width="12" y="20" x="100"></rect>
+  <text id="text" fill="black" font-size="12" font-family="Arial" y="30" x="115">Apple</text> 
+  <path id="path" d="M 200 26 L 600 26" stroke="red" stroke-width="1em"/>
+</svg>
 </body>
 </html>
diff --git a/javascript/atoms/test/test_util.js b/javascript/atoms/test/test_util.js
index 5a620d0..df2ca36 100644
--- a/javascript/atoms/test/test_util.js
+++ b/javascript/atoms/test/test_util.js
@@ -19,10 +19,22 @@
 
 goog.provide('bot.test');
 
+goog.require('bot.userAgent');
 goog.require('goog.userAgent');
 
 
 /**
+ * Whether the browser supports SVG element inline with HTML.
+ *
+ * @const
+ * @type {boolean}
+ */
+bot.test.SUPPORTS_INLINE_SVG = !bot.userAgent.IE_DOC_PRE9 &&
+    !bot.userAgent.ANDROID_PRE_GINGERBREAD &&
+    !(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion(2));
+
+
+/**
  * @return {boolean} Whether the window under test has focus.
  */
 bot.test.isWindowFocused = function() {
diff --git a/javascript/atoms/test/testdata/nested_scrolling_iframe_parent.html b/javascript/atoms/test/testdata/nested_scrolling_iframe_parent.html
index 91bdfcb..9301cfa 100644
--- a/javascript/atoms/test/testdata/nested_scrolling_iframe_parent.html
+++ b/javascript/atoms/test/testdata/nested_scrolling_iframe_parent.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
 <head>
   <title>Nested scrollable</title>
diff --git a/javascript/atoms/test/testdata/simple.html b/javascript/atoms/test/testdata/simple.html
index 259c613..7377200 100644
--- a/javascript/atoms/test/testdata/simple.html
+++ b/javascript/atoms/test/testdata/simple.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
   <body>
     <a href="#" id="in-offscreen-in-view-iframe">Click</a>
diff --git a/javascript/atoms/test/touchscreen_test.html b/javascript/atoms/test/touchscreen_test.html
index c940cb1..887d7f2 100644
--- a/javascript/atoms/test/touchscreen_test.html
+++ b/javascript/atoms/test/touchscreen_test.html
@@ -23,8 +23,15 @@
       goog.events.EventType.TOUCHMOVE,
       goog.events.EventType.MOUSEMOVE,
       goog.events.EventType.MOUSEDOWN,
+      goog.events.EventType.MOUSEOUT,
+      goog.events.EventType.MOUSEOVER,
       goog.events.EventType.MOUSEUP,
-      goog.events.EventType.CLICK
+      goog.events.EventType.CLICK,
+      goog.events.EventType.MSPOINTERDOWN,
+      goog.events.EventType.MSPOINTERMOVE,
+      goog.events.EventType.MSPOINTEROVER,
+      goog.events.EventType.MSPOINTEROUT,
+      goog.events.EventType.MSPOINTERUP
     ];
 
     function setUpPage() {
@@ -53,7 +60,29 @@
       events = [];
     }
 
-    function assertEvents(expectedEvents) {
+    function msPointerDownEvents(elem) {
+        return [goog.events.EventType.MOUSEMOVE, target].concat(
+                   [goog.events.EventType.MSPOINTEROVER, target],
+                   [goog.events.EventType.MOUSEOVER, target],
+                   [goog.events.EventType.MSPOINTERDOWN, target],
+                   [goog.events.EventType.MOUSEDOWN, target]);
+    }
+
+    function msPointerMoveEvents(elem) {
+        return [goog.events.EventType.MSPOINTERMOVE, target].concat(
+                   [goog.events.EventType.MOUSEMOVE, target]);
+    }
+
+    function msPointerUpEvents(elem) {
+        return [goog.events.EventType.MSPOINTERUP, target].concat(
+                   [goog.events.EventType.MOUSEUP, target],
+                   [goog.events.EventType.CLICK, target],
+                   [goog.events.EventType.MSPOINTEROUT, target],
+                   [goog.events.EventType.MOUSEOUT, target]);
+    }
+
+    function assertEvents(var_args) {
+      var expectedEvents = goog.array.concat.apply(null, arguments);
       assertArrayEquals(expectedEvents, events);
       events = [];
     }
@@ -65,7 +94,12 @@
       touchscreen.move(target, new goog.math.Coordinate(0, 0));
       touchscreen.press();
       assertTrue(touchscreen.isPressed());
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 1]);
+
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 1]);
+      }
     }
 
     function testTouchScreen2FingerPress() {
@@ -77,7 +111,12 @@
                        new goog.math.Coordinate(10, 10));
       touchscreen.press(/* opt_press2 */ true);
       assertTrue(touchscreen.isPressed());
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 2]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerDownEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 2]);
+      }
     }
 
     function testTouchScreen2FingerPinch() {
@@ -92,9 +131,18 @@
                        new goog.math.Coordinate(0, 10),
                        new goog.math.Coordinate(0, 40));
       touchscreen.release();
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 2,
-                    goog.events.EventType.TOUCHMOVE, target, 2,
-                    goog.events.EventType.TOUCHEND, target, 2]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerDownEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerUpEvents(target),
+                     msPointerUpEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 2,
+                      goog.events.EventType.TOUCHMOVE, target, 2,
+                      goog.events.EventType.TOUCHEND, target, 2]);
+      }
     }
 
     function testTouchScreen2FingerRotate() {
@@ -108,10 +156,21 @@
       touchscreen.move(target, new goog.math.Coordinate(10, 40));
       touchscreen.move(target, new goog.math.Coordinate(50, 50));
       touchscreen.release();
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 2,
-                    goog.events.EventType.TOUCHMOVE, target, 2,
-                    goog.events.EventType.TOUCHMOVE, target, 2,
-                    goog.events.EventType.TOUCHEND, target, 2]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerDownEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerUpEvents(target),
+                     msPointerUpEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 2,
+                      goog.events.EventType.TOUCHMOVE, target, 2,
+                      goog.events.EventType.TOUCHMOVE, target, 2,
+                      goog.events.EventType.TOUCHEND, target, 2]);
+      }
     }
 
     function testTouchScreenRelease() {
@@ -122,12 +181,16 @@
       touchscreen.press();
       touchscreen.release();
       assertFalse(touchscreen.isPressed());
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
-                    goog.events.EventType.TOUCHEND, target, 1,
-                    goog.events.EventType.MOUSEMOVE, target,
-                    goog.events.EventType.MOUSEDOWN, target,
-                    goog.events.EventType.MOUSEUP, target,
-                    goog.events.EventType.CLICK, target]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target), msPointerUpEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
+                      goog.events.EventType.TOUCHEND, target, 1,
+                      goog.events.EventType.MOUSEMOVE, target,
+                      goog.events.EventType.MOUSEDOWN, target,
+                      goog.events.EventType.MOUSEUP, target,
+                      goog.events.EventType.CLICK, target]);
+      }
     }
 
     function testTouchScreenReleaseAfterMove() {
@@ -139,9 +202,15 @@
       // After a move, the mouseclick should not happen.
       touchscreen.move(target, new goog.math.Coordinate(10, 10));
       touchscreen.release();
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
-                    goog.events.EventType.TOUCHMOVE, target, 1,
-                    goog.events.EventType.TOUCHEND, target, 1]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerUpEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
+                      goog.events.EventType.TOUCHMOVE, target, 1,
+                      goog.events.EventType.TOUCHEND, target, 1]);
+      }
     }
 
     function testTouchScreenReleaseAfterNewElement() {
@@ -154,9 +223,15 @@
       // events should fire on the element where the touch started.
       touchscreen.move(lowerTarget, new goog.math.Coordinate(0, 0));
       touchscreen.release();
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
-                    goog.events.EventType.TOUCHMOVE, target, 1,
-                    goog.events.EventType.TOUCHEND, target, 1]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerMoveEvents(target),
+                     msPointerUpEvents(lowerTarget));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
+                      goog.events.EventType.TOUCHMOVE, target, 1,
+                      goog.events.EventType.TOUCHEND, target, 1]);
+      }
     }
 
     function testTouchScreenMoveWithoutPress() {
@@ -175,8 +250,13 @@
       touchscreen.press();
       touchscreen.move(target, new goog.math.Coordinate(20, 20));
       assertTrue(touchscreen.isPressed());
-      assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
-                    goog.events.EventType.TOUCHMOVE, target, 1]);
+      if (bot.userAgent.IE_DOC_10) {
+        assertEvents(msPointerDownEvents(target),
+                     msPointerMoveEvents(target));
+      } else {
+        assertEvents([goog.events.EventType.TOUCHSTART, target, 1,
+                      goog.events.EventType.TOUCHMOVE, target, 1]);
+      }
     }
 
     function testExceptionReleasingWhenNotPressed() {
@@ -193,7 +273,7 @@
       }
       touchscreen.move(target, new goog.math.Coordinate(0, 0));
       touchscreen.press();
-      assertThrows('Pressing when alread pressed should throw an exception',
+      assertThrows('Pressing when already pressed should throw an exception',
                    goog.bind(touchscreen.press, touchscreen));
     }
   </script>
diff --git a/javascript/atoms/test/useragent_quirks_test.html b/javascript/atoms/test/useragent_quirks_test.html
new file mode 100644
index 0000000..aae45de
--- /dev/null
+++ b/javascript/atoms/test/useragent_quirks_test.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+  <title>useragent_quirks_test</title>
+  <script src="test_bootstrap.js"></script>
+  <script type="text/javascript">
+    goog.require('bot.userAgent');
+    goog.require('goog.dom');
+    goog.require('goog.testing.jsunit');
+    goog.require('goog.userAgent');
+    goog.require('goog.userAgent.product');
+  </script>
+  <script src="useragent_test.js" type="text/javascript"></script>
+</head>
+</html>
diff --git a/javascript/atoms/test/useragent_test.html b/javascript/atoms/test/useragent_test.html
new file mode 100644
index 0000000..ef295e8
--- /dev/null
+++ b/javascript/atoms/test/useragent_test.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>useragent_test</title>
+  <script src="test_bootstrap.js"></script>
+  <script type="text/javascript">
+    goog.require('bot.userAgent');
+    goog.require('goog.dom');
+    goog.require('goog.testing.jsunit');
+    goog.require('goog.userAgent');
+    goog.require('goog.userAgent.product');
+  </script>
+  <script src="useragent_test.js" type="text/javascript"></script>
+</head>
+</html>
diff --git a/javascript/atoms/test/useragent_test.js b/javascript/atoms/test/useragent_test.js
new file mode 100644
index 0000000..a69349b
--- /dev/null
+++ b/javascript/atoms/test/useragent_test.js
@@ -0,0 +1,61 @@
+/**
+ * @fileoverview Common user agent tests.
+ * @author joonlee@google.com (Joon Lee)
+ */
+
+goog.require('goog.dom');
+goog.require('goog.userAgent');
+goog.require('goog.userAgent.product');
+
+var productVersion = parseFloat(goog.userAgent.product.ANDROID ?
+    bot.userAgent.ANDROID_VERSION_ : goog.userAgent.product.VERSION);
+
+var engineVersion = parseFloat(goog.userAgent.VERSION);
+if (goog.userAgent.IE && !goog.dom.isCss1CompatMode() && engineVersion < 10) {
+  engineVersion = 5;
+}
+
+function testIsEngineVersion() {
+  assertTrue(bot.userAgent.isEngineVersion(engineVersion));
+}
+
+function testIsEngineVersionLower() {
+  assertTrue(bot.userAgent.isEngineVersion(engineVersion - 1));
+}
+
+function testIsEngineVersionLittleHigher() {
+  assertFalse(bot.userAgent.isEngineVersion(engineVersion + 0.111));
+}
+
+function testIsEngineVersionHigher() {
+  assertFalse(bot.userAgent.isEngineVersion(engineVersion + 1));
+}
+
+function testIsEngineVersionLetters() {
+  assertTrue(bot.userAgent.isEngineVersion(engineVersion + 'a'));
+}
+
+function testIsProductVersion() {
+  assertTrue(bot.userAgent.isProductVersion(productVersion));
+}
+
+function testIsProductVersionLower() {
+  assertTrue(bot.userAgent.isProductVersion(productVersion - 1));
+}
+
+function testIsProductVersionHigher() {
+  assertFalse(bot.userAgent.isProductVersion(productVersion + 1));
+}
+
+function testProductVersionAtLeastEngineVersion_IE() {
+  if (goog.userAgent.IE) {
+    assertTrue(bot.userAgent.isProductVersion(engineVersion));
+  }
+}
+
+function testEngineVersionIsMajorProductVersionInStandardsMode_IE() {
+  if (goog.userAgent.IE && goog.dom.isCss1CompatMode()) {
+    var majorProductVersion = Math.floor(productVersion);
+    assertTrue(bot.userAgent.isEngineVersion(majorProductVersion));
+  }
+}
diff --git a/javascript/atoms/touchscreen.js b/javascript/atoms/touchscreen.js
index 4da27fd..677357d 100644
--- a/javascript/atoms/touchscreen.js
+++ b/javascript/atoms/touchscreen.js
@@ -106,7 +106,11 @@
     this.touchIdentifier2_ = this.touchCounter_++;
   }
 
-  this.fireTouchEvent_(bot.events.EventType.TOUCHSTART);
+  if (bot.userAgent.IE_DOC_10) {
+    this.firePointerEvents_(bot.Touchscreen.fireSinglePressPointer_);
+  } else {
+    this.fireTouchEvent_(bot.events.EventType.TOUCHSTART);
+  }
 };
 
 
@@ -120,26 +124,10 @@
         'Cannot release touchscreen when not already pressed.');
   }
 
-  this.fireTouchEvent_(bot.events.EventType.TOUCHEND);
-
-  // If no movement occurred since press, TouchScreen.Release will fire the
-  // legacy mouse events: mousemove, mousedown, mouseup, and click
-  // after the touch events have been fired. The click button should be zero
-  // and only one mousemove should fire.
-  if (!this.hasMovedAfterPress_) {
-    this.fireMouseEvent(bot.events.EventType.MOUSEMOVE, this.clientXY_, 0);
-    var performFocus = this.fireMouseEvent(bot.events.EventType.MOUSEDOWN,
-                                           this.clientXY_, 0);
-    // Element gets focus after the mousedown event only if the mousedown was
-    // not cancelled.
-    if (performFocus) {
-      this.focusOnElement();
-    }
-
-    this.fireMouseEvent(bot.events.EventType.MOUSEUP, this.clientXY_, 0);
-
-    // Special click logic to follow links and to perform form actions.
-    this.clickElement(this.clientXY_, /* button value */ 0);
+  if (bot.userAgent.IE_DOC_10) {
+    this.firePointerEvents_(bot.Touchscreen.fireSingleReleasePointer_);
+  } else {
+    this.fireTouchReleaseEvents_();
   }
   this.touchIdentifier_ = 0;
   this.touchIdentifier2_ = 0;
@@ -158,7 +146,8 @@
 bot.Touchscreen.prototype.move = function(element, coords, opt_coords2) {
   // The target element for touch actions is the original element. Hence, the
   // element is set only when the touchscreen is not currently being pressed.
-  if (!this.isPressed()) {
+  // The exception is IE10 which fire events on the moved to element.
+  if (!this.isPressed() || bot.userAgent.IE_DOC_10) {
     this.setElement(element);
   }
 
@@ -173,7 +162,11 @@
 
   if (this.isPressed()) {
     this.hasMovedAfterPress_ = true;
-    this.fireTouchEvent_(bot.events.EventType.TOUCHMOVE);
+    if (bot.userAgent.IE_DOC_10) {
+      this.firePointerEvents_(bot.Touchscreen.fireSingleMovePointer_);
+    } else {
+      this.fireTouchEvent_(bot.events.EventType.TOUCHMOVE);
+    }
   }
 };
 
@@ -192,7 +185,6 @@
  * A helper function to fire touch events.
  *
  * @param {bot.events.EventType} type Event type.
- * @return {boolean} Whether the event fired successfully or was cancelled.
  * @private
  */
 bot.Touchscreen.prototype.fireTouchEvent_ = function(type) {
@@ -206,6 +198,127 @@
     touchIdentifier2 = this.touchIdentifier2_;
     coords2 = this.clientXY2_;
   }
-  return this.fireTouchEvent(type, this.touchIdentifier_, this.clientXY_,
-                             touchIdentifier2, coords2);
+  this.fireTouchEvent(type, this.touchIdentifier_, this.clientXY_,
+                      touchIdentifier2, coords2);
+};
+
+
+/**
+ * A helper function to fire touch events that occur on a release.
+ *
+ * @private
+ */
+bot.Touchscreen.prototype.fireTouchReleaseEvents_ = function() {
+  this.fireTouchEvent_(bot.events.EventType.TOUCHEND);
+
+  // If no movement occurred since press, TouchScreen.Release will fire the
+  // legacy mouse events: mousemove, mousedown, mouseup, and click
+  // after the touch events have been fired. The click button should be zero
+  // and only one mousemove should fire.
+  if (!this.hasMovedAfterPress_) {
+    this.fireMouseEvent(bot.events.EventType.MOUSEMOVE, this.clientXY_, 0);
+    var performFocus = this.fireMouseEvent(bot.events.EventType.MOUSEDOWN,
+                                           this.clientXY_, 0);
+    // Element gets focus after the mousedown event only if the mousedown was
+    // not cancelled.
+    if (performFocus) {
+      this.focusOnElement();
+    }
+    this.fireMouseEvent(bot.events.EventType.MOUSEUP, this.clientXY_, 0);
+
+    // Special click logic to follow links and to perform form actions.
+    this.clickElement(this.clientXY_, /* button value */ 0);
+  }
+};
+
+
+/**
+ * A helper function to fire a sequence of Pointer events.
+ * @param {function(!bot.Touchscreen, !goog.math.Coordinate, number, boolean)}
+ *     fireSinglePointer A function that fires a set of events for one finger.
+ * @private
+ */
+bot.Touchscreen.prototype.firePointerEvents_ = function(fireSinglePointer) {
+  fireSinglePointer(this, this.clientXY_, this.touchIdentifier_, true);
+  if (this.touchIdentifier2_) {
+    fireSinglePointer(this, this.clientXY2_, this.touchIdentifier2_, false);
+  }
+};
+
+
+/**
+ * A helper function to fire Pointer events related to a press.
+ *
+ * @param {!bot.Touchscreen} ts A touchscreen object.
+ * @param {!goog.math.Coordinate} coords Coordinates relative to
+ *   currentElement.
+ * @param {number} id The touch identifier.
+ * @param {boolean} isPrimary Whether the pointer represents the primary point
+ *     of contact.
+ * @private
+ */
+bot.Touchscreen.fireSinglePressPointer_ = function(ts, coords, id, isPrimary) {
+  // Fire a mousemove event.
+  ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0);
+
+  // Fire a MSPointerOver and mouseover events.
+  ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROVER, coords, 0, id,
+      MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
+  ts.fireMouseEvent(bot.events.EventType.MOUSEOVER, coords, 0);
+
+  // Fire a MSPointerDown and mousedown events.
+  ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERDOWN, coords, 0, id,
+      MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
+
+  // Element gets focus after the mousedown event.
+  if (ts.fireMouseEvent(bot.events.EventType.MOUSEDOWN, coords, 0)) {
+    ts.focusOnElement();
+  }
+};
+
+
+/**
+ * A helper function to fire Pointer events related to a release.
+ *
+ * @param {!bot.Touchscreen} ts A touchscreen object.
+ * @param {!goog.math.Coordinate} coords Coordinates relative to
+ *   currentElement.
+ * @param {number} id The touch identifier.
+ * @param {boolean} isPrimary Whether the pointer represents the primary point
+ *     of contact.
+ * @private
+ */
+bot.Touchscreen.fireSingleReleasePointer_ = function(ts, coords, id,
+                                                     isPrimary) {
+  // Fire a MSPointerUp and mouseup events.
+  ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERUP, coords, 0, id,
+      MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
+  ts.fireMouseEvent(bot.events.EventType.MOUSEUP, coords, 0);
+
+  // Fire a click.
+  ts.clickElement(coords, 0);
+
+  // Fire a MSPointerOut and mouseout events.
+  ts.fireMSPointerEvent(bot.events.EventType.MSPOINTEROUT, coords, -1, id,
+      MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
+  ts.fireMouseEvent(bot.events.EventType.MOUSEOUT, coords, 0);
+};
+
+
+/**
+ * A helper function to fire Pointer events related to a move.
+ *
+ * @param {!bot.Touchscreen} ts A touchscreen object.
+ * @param {!goog.math.Coordinate} coords Coordinates relative to
+ *   currentElement.
+ * @param {number} id The touch identifier.
+ * @param {boolean} isPrimary Whether the pointer represents the primary point
+ *     of contact.
+ * @private
+ */
+bot.Touchscreen.fireSingleMovePointer_ = function(ts, coords, id, isPrimary) {
+  // Fire a MSPointerMove and mousemove events.
+  ts.fireMSPointerEvent(bot.events.EventType.MSPOINTERMOVE, coords, -1, id,
+      MSPointerEvent.MSPOINTER_TYPE_TOUCH, isPrimary);
+  ts.fireMouseEvent(bot.events.EventType.MOUSEMOVE, coords, 0);
 };
diff --git a/javascript/atoms/userAgent.js b/javascript/atoms/userAgent.js
index 04ea0bb..a808ef1 100644
--- a/javascript/atoms/userAgent.js
+++ b/javascript/atoms/userAgent.js
@@ -43,7 +43,8 @@
   if (bot.userAgent.FIREFOX_EXTENSION) {
     return bot.userAgent.FIREFOX_EXTENSION_IS_ENGINE_VERSION_(version);
   } else if (goog.userAgent.IE) {
-    return goog.string.compareVersions(document.documentMode, version) >= 0;
+    return goog.string.compareVersions(
+        /** @type {number} */ (goog.userAgent.DOCUMENT_MODE), version) >= 0;
   } else {
     return goog.userAgent.isVersion(version);
   }
@@ -183,11 +184,20 @@
 
 
 /**
+ * Whether the current document is IE in a documentMode older than 8.
+ * @type {boolean}
+ * @const
+ */
+bot.userAgent.IE_DOC_PRE8 = goog.userAgent.IE &&
+    !goog.userAgent.isDocumentMode(8);
+
+
+/**
  * Whether the current document is IE in IE9 (or newer) standards mode.
  * @type {boolean}
  * @const
  */
-bot.userAgent.IE_DOC_9 = goog.userAgent.IE && document.documentMode >= 9;
+bot.userAgent.IE_DOC_9 = goog.userAgent.isDocumentMode(9);
 
 
 /**
@@ -195,4 +205,31 @@
  * @type {boolean}
  * @const
  */
-bot.userAgent.IE_DOC_PRE9 = goog.userAgent.IE && !bot.userAgent.IE_DOC_9;
+bot.userAgent.IE_DOC_PRE9 = goog.userAgent.IE &&
+    !goog.userAgent.isDocumentMode(9);
+
+
+/**
+ * Whether the current document is IE in IE10 (or newer) standards mode.
+ * @type {boolean}
+ * @const
+ */
+bot.userAgent.IE_DOC_10 = goog.userAgent.isDocumentMode(10);
+
+
+/**
+ * Whether the current document is IE in a documentMode older than 10.
+ * @type {boolean}
+ * @const
+ */
+bot.userAgent.IE_DOC_PRE10 = goog.userAgent.IE &&
+    !goog.userAgent.isDocumentMode(10);
+
+
+/**
+ * Whether the current browser is Android pre-gingerbread.
+ * @type {boolean}
+ * @const
+ */
+bot.userAgent.ANDROID_PRE_GINGERBREAD = goog.userAgent.product.ANDROID &&
+    !bot.userAgent.isProductVersion(2.3);
diff --git a/javascript/firefox-driver/extension/content/server.js b/javascript/firefox-driver/extension/content/server.js
index 5fe60a3..33565c1 100644
--- a/javascript/firefox-driver/extension/content/server.js
+++ b/javascript/firefox-driver/extension/content/server.js
@@ -34,8 +34,8 @@
 // This will configure a FirefoxDriver and DomMessenger for each
 // _browser window_ (not chrome window). Multiple tabs in the same window will
 // share a FirefoxDriver and DomMessenger instance.
-window.addEventListener("load", function(e) {
-  handle = Components.classes["@googlecode.com/webdriver/fxdriver;1"].createInstance();
+window.addEventListener('load', function(e) {
+  handle = Components.classes['@googlecode.com/webdriver/fxdriver;1'].createInstance();
   var server = handle.wrappedJSObject;
 
   if (!domMessenger) {
diff --git a/javascript/firefox-driver/js/badCertListener.js b/javascript/firefox-driver/js/badCertListener.js
index da949a9..28e54e1 100644
--- a/javascript/firefox-driver/js/badCertListener.js
+++ b/javascript/firefox-driver/js/badCertListener.js
@@ -25,14 +25,14 @@
 
 goog.provide('WdCertOverrideService');
 
+goog.require('bot.userAgent');
 goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
-goog.require('bot.userAgent');
 
 
 function getPreferenceFromProfile(prefName, prefDefaultValue) {
   var prefs =
-      CC["@mozilla.org/preferences-service;1"].getService(CI["nsIPrefBranch"]);
+      CC['@mozilla.org/preferences-service;1'].getService(CI['nsIPrefBranch']);
 
   if (!prefs.prefHasUserValue(prefName)) {
     fxdriver.logging.info(prefName + ' not set; defaulting to ' + prefDefaultValue);
@@ -40,7 +40,7 @@
   }
 
   var prefValue = prefs.getBoolPref(prefName);
-  fxdriver.logging.info("Found preference for " + prefName + ": " + prefValue);
+  fxdriver.logging.info('Found preference for ' + prefName + ': ' + prefValue);
 
   return prefValue;
 }
@@ -49,11 +49,11 @@
   // Defaults to true - accepting untrusted certificates.
   // This puts the module into effect - setting it to false
   // will delegate all calls to the original service.
-  return getPreferenceFromProfile("webdriver_accept_untrusted_certs", true);
+  return getPreferenceFromProfile('webdriver_accept_untrusted_certs', true);
 }
 
 function shouldAssumeUntrustedIssuer() {
-  return getPreferenceFromProfile("webdriver_assume_untrusted_issuer", true);
+  return getPreferenceFromProfile('webdriver_assume_untrusted_issuer', true);
 }
 
 WdCertOverrideService = function() {
@@ -69,10 +69,10 @@
     this.default_bits = 0;
   }
 
-  fxdriver.logging.info("Accept untrusted certificates: " + shouldAcceptUntrustedCerts());
+  fxdriver.logging.info('Accept untrusted certificates: ' + shouldAcceptUntrustedCerts());
 
   // UUID of the original implementor of this service.
-  var ORIGINAL_OVERRIDE_SERVICE_ID = "{67ba681d-5485-4fff-952c-2ee337ffdcd6}";
+  var ORIGINAL_OVERRIDE_SERVICE_ID = '{67ba681d-5485-4fff-952c-2ee337ffdcd6}';
 
   // Keep a reference to the original bad certificate listener.
   var originalService = Components.classesByID[ORIGINAL_OVERRIDE_SERVICE_ID].
@@ -92,10 +92,10 @@
 };
 
 // Returns the bit needed to mask if the certificate has expired, 0 otherwise.
-WdCertOverrideService.prototype.certificateExpiredBit_ = function
-  (theCert, verification_result) {
+WdCertOverrideService.prototype.certificateExpiredBit_ =
+    function(theCert, verification_result) {
   if ((verification_result & theCert.CERT_EXPIRED) != 0) {
-    fxdriver.logging.info("Certificate expired.");
+    fxdriver.logging.info('Certificate expired.');
     return this.ERROR_TIME;
   }
 
@@ -104,17 +104,17 @@
 
 // Returns the bit needed to mask untrusted issuers, 0 otherwise.
 // Note that this bit is already set by default in default_bits
-WdCertOverrideService.prototype.certificateIssuerUntrusted_ = function
-  (theCert, verification_result) {
+WdCertOverrideService.prototype.certificateIssuerUntrusted_ =
+    function(theCert, verification_result) {
   if (((verification_result & theCert.ISSUER_UNKNOWN) != 0) ||
       ((verification_result & theCert.ISSUER_NOT_TRUSTED) != 0) ||
       ((verification_result & theCert.CERT_NOT_TRUSTED) != 0) ||
       ((verification_result & theCert.INVALID_CA) != 0)) {
-    fxdriver.logging.info("Certificate issuer unknown.");
-    fxdriver.logging.info("Unknown: " + (theCert.ISSUER_UNKNOWN & verification_result));
-    fxdriver.logging.info("Issuer not trusted: " + (theCert.ISSUER_NOT_TRUSTED & verification_result));
-    fxdriver.logging.info("Cert not trusted: " + (theCert.CERT_NOT_TRUSTED & verification_result));
-    fxdriver.logging.info("Invalid CA: " + (theCert.INVALID_CA & verification_result));
+    fxdriver.logging.info('Certificate issuer unknown.');
+    fxdriver.logging.info('Unknown: ' + (theCert.ISSUER_UNKNOWN & verification_result));
+    fxdriver.logging.info('Issuer not trusted: ' + (theCert.ISSUER_NOT_TRUSTED & verification_result));
+    fxdriver.logging.info('Cert not trusted: ' + (theCert.CERT_NOT_TRUSTED & verification_result));
+    fxdriver.logging.info('Invalid CA: ' + (theCert.INVALID_CA & verification_result));
 
     return this.ERROR_UNTRUSTED;
   }
@@ -124,11 +124,11 @@
 
 // Returns the bit needed to mask mismatch between actual hostname
 // and the hostname the certificate was issued for, 0 otherwise.
-WdCertOverrideService.prototype.certificateHostnameMismatch_ = function
-  (theCert, aHost) {
-  var commonNameRE = new RegExp("^" + theCert.commonName.replace('*', '[\\w|\-]+') + "$", "i");
+WdCertOverrideService.prototype.certificateHostnameMismatch_ =
+    function(theCert, aHost) {
+  var commonNameRE = new RegExp('^' + theCert.commonName.replace('*', '[\\w|\-]+') + '$', 'i');
   if (aHost.match(commonNameRE) === null) {
-    fxdriver.logging.info("Host name mismatch: cert: " + theCert.commonName + " get: " + aHost);
+    fxdriver.logging.info('Host name mismatch: cert: ' + theCert.commonName + ' get: ' + aHost);
     return this.ERROR_MISMATCH;
   }
 
@@ -136,7 +136,7 @@
 };
 
 // Given a certificate and the host it was received for, fill in the bits
-// needed to accept this certificate for this host, even though the 
+// needed to accept this certificate for this host, even though the
 // certificate is invalid.
 //
 // Note that the bitmask has to be accurate: At the moment, Firefox expects
@@ -147,7 +147,7 @@
   var verification_bits = aCert.verifyForUsage(aCert.CERT_USAGE_SSLClient);
   var return_bits = this.default_bits;
 
-  fxdriver.logging.info("Certificate verification results: " + verification_bits);
+  fxdriver.logging.info('Certificate verification results: ' + verification_bits);
 
   return_bits = return_bits | this.certificateExpiredBit_(
       aCert, verification_bits);
@@ -159,12 +159,12 @@
   // It has been observed that if there's a host name mismatch then it
   // may not be required to check the trust status of the certificate issuer.
   if (return_bits == 0) {
-    fxdriver.logging.info("Checking issuer since certificate has not expired or has a host name mismatch.");
+    fxdriver.logging.info('Checking issuer since certificate has not expired or has a host name mismatch.');
     return_bits = return_bits | this.certificateIssuerUntrusted_(
         aCert, verification_bits);
   }
 
-  fxdriver.logging.info("return_bits now: " + return_bits);
+  fxdriver.logging.info('return_bits now: ' + return_bits);
   return return_bits;
 };
 
@@ -175,12 +175,12 @@
 
   var acceptAll = shouldAcceptUntrustedCerts();
   if (acceptAll === true) {
-    fxdriver.logging.info("Allowing certificate from site: " + aHostName + ":" + aPort);
+    fxdriver.logging.info('Allowing certificate from site: ' + aHostName + ':' + aPort);
     retval = true;
     aIsTemporary.value = false;
 
     aOverrideBits.value = this.fillNeededBits(aCert, aHostName);
-    fxdriver.logging.info("Override Bits: " + aOverrideBits.value);
+    fxdriver.logging.info('Override Bits: ' + aOverrideBits.value);
   } else {
     retval = this.origListener_.hasMatchingOverride(aHostName, aPort,
               aCert, aOverrideBits, aIsTemporary);
@@ -230,7 +230,7 @@
 };
 
 // Service contract ID which we override
-/** @const */ var CERTOVERRIDE_CONTRACT_ID = "@mozilla.org/security/certoverride;1";
+/** @const */ var CERTOVERRIDE_CONTRACT_ID = '@mozilla.org/security/certoverride;1';
 // UUID for this instance specifically.
 /** @const */ var DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID =
   Components.ID('{c8fffaba-9b7a-41aa-872d-7e7366c16715}');
@@ -238,7 +238,7 @@
 var service = undefined;
 
 var WDCertOverrideFactory = {
-  createInstance: function (aOuter, aIID) {
+  createInstance: function(aOuter, aIID) {
     if (aOuter != null)
       throw CR['NS_ERROR_NO_AGGREGATION'];
     if (service == undefined) {
@@ -260,17 +260,17 @@
     throw CR['NS_ERROR_FACTORY_REGISTER_AGAIN'];
   }
 
-  fxdriver.logging.info("Registering Override Certificate service.");
+  fxdriver.logging.info('Registering Override Certificate service.');
   aCompMgr = aCompMgr.QueryInterface(
       CI['nsIComponentRegistrar']);
   aCompMgr.registerFactoryLocation(
-      DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID, "badCertListener",
+      DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID, 'badCertListener',
       CERTOVERRIDE_CONTRACT_ID, aFileSpec, aLocation, aType);
 };
 
 WDBadCertListenerModule.prototype.unregisterSelf = function(
     aCompMgr, aLocation, aType) {
-  fxdriver.logging.info("Un-registering Override Certificate service.");
+  fxdriver.logging.info('Un-registering Override Certificate service.');
   aCompMgr =
   aCompMgr.QueryInterface(CI['nsIComponentRegistrar']);
   aCompMgr.unregisterFactoryLocation(DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID, aLocation);
@@ -279,9 +279,9 @@
 
 var FACTORY;
 
-if (!bot.userAgent.isProductVersion('10')){
+if (!bot.userAgent.isProductVersion('10')) {
 /** @const */ FACTORY = {
-  createInstance: function (aOuter, aIID) {
+  createInstance: function(aOuter, aIID) {
     if (aOuter != null)
       throw CR['NS_ERROR_NO_AGGREGATION'];
 
@@ -291,8 +291,8 @@
 
     var raw = new WdCertOverrideService();
 
-    var mainThread = CC["@mozilla.org/thread-manager;1"].getService().mainThread;
-    var proxyManager = CC["@mozilla.org/xpcomproxy;1"]
+    var mainThread = CC['@mozilla.org/thread-manager;1'].getService().mainThread;
+    var proxyManager = CC['@mozilla.org/xpcomproxy;1']
         .getService(CI.nsIProxyObjectManager);
 
     // 5 == NS_PROXY_ALWAYS | NS_PROXY_SYNC
@@ -313,8 +313,8 @@
 
   if (aCID.equals(DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID)) {
     if (bot.userAgent.isProductVersion('10')) {
-      return WDCertOverrideFactory; 
-    } else{
+      return WDCertOverrideFactory;
+    } else {
       return FACTORY;
    }
   }
@@ -332,7 +332,7 @@
 
 
 WdCertOverrideService.prototype.classID = DUMMY_CERTOVERRIDE_SERVICE_CLASS_ID;
-fxdriver.moz.load("resource://gre/modules/XPCOMUtils.jsm");
+fxdriver.moz.load('resource://gre/modules/XPCOMUtils.jsm');
 if (XPCOMUtils.generateNSGetFactory) {
   /** @const */ NSGetFactory = XPCOMUtils.generateNSGetFactory([WdCertOverrideService]);
 }
diff --git a/javascript/firefox-driver/js/dispatcher.js b/javascript/firefox-driver/js/dispatcher.js
index 1317deb..6bae4b6 100644
--- a/javascript/firefox-driver/js/dispatcher.js
+++ b/javascript/firefox-driver/js/dispatcher.js
@@ -19,12 +19,12 @@
 goog.provide('Dispatcher');
 goog.provide('Resource');
 
+goog.require('Request');
+goog.require('Response');
+goog.require('Utils');
 goog.require('bot.ErrorCode');
 goog.require('fxdriver.error');
 goog.require('fxdriver.logging');
-goog.require('Response');
-goog.require('Request');
-goog.require('Utils');
 
 
 /**
@@ -121,7 +121,7 @@
         var url = request.getRequestUrl();
         response.setStatus(Response.SEE_OTHER);
         response.setHeader('Location',
-            url.scheme + '://' + url.host + ":" + url.hostPort + url.path + '/' +
+            url.scheme + '://' + url.host + ':' + url.hostPort + url.path + '/' +
                 jsonResponse.value);
         response.commit();
       }
@@ -329,7 +329,7 @@
       on(Request.Method.GET, Dispatcher.executeAs('isOnline'));
 
   this.bind_('/session/:sessionId/application_cache/status').
-      on(Request.Method.GET, Dispatcher.executeAs('getAppCacheStatus'))
+      on(Request.Method.GET, Dispatcher.executeAs('getAppCacheStatus'));
 
   // --------------------------------------------------------------------------
   // Firefox extensions to the wire protocol.
diff --git a/javascript/firefox-driver/js/events.js b/javascript/firefox-driver/js/events.js
index 835ae14..c0b8884 100644
--- a/javascript/firefox-driver/js/events.js
+++ b/javascript/firefox-driver/js/events.js
@@ -18,10 +18,10 @@
 
 goog.provide('fxdriver.events');
 
+goog.require('Utils');
 goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
 goog.require('goog.style');
-goog.require('Utils');
 
 
 /**
@@ -43,8 +43,8 @@
 
   if (goog.isNull(x) && element) {
     var size = goog.style.getSize(element);
-    x =  size.width / 2;
-    y =  size.height / 2;
+    x = size.width / 2;
+    y = size.height / 2;
   }
 
   return {
diff --git a/javascript/firefox-driver/js/firefox-utils.js b/javascript/firefox-driver/js/firefox-utils.js
index d1eed50..00e5851 100644
--- a/javascript/firefox-driver/js/firefox-utils.js
+++ b/javascript/firefox-driver/js/firefox-utils.js
@@ -24,9 +24,9 @@
 
 goog.provide('fxdriver.utils');
 
-goog.require('bot.userAgent');
 goog.require('bot.Error');
 goog.require('bot.ErrorCode');
+goog.require('bot.userAgent');
 goog.require('fxdriver.moz');
 goog.require('goog.array');
 
@@ -89,16 +89,16 @@
   // TODO(simon): initialize this statically.
   if (!fxdriver.utils._generator) {
     fxdriver.utils._generator =
-    fxdriver.moz.getService("@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+    fxdriver.moz.getService('@mozilla.org/uuid-generator;1', 'nsIUUIDGenerator');
   }
   return fxdriver.utils._generator.generateUUID().toString();
 };
 
 
 /**
- * @param {!Element} element The element to use
- * @param {int} x X coordinate
- * @param {int} y Y coordinate
+ * @param {!Element} element The element to use.
+ * @param {int} x X coordinate.
+ * @param {int} y Y coordinate.
  */
 fxdriver.utils.newCoordinates = function(element, x, y) {
   return {
@@ -112,4 +112,4 @@
     x: x,
     y: y
   };
-}
+};
diff --git a/javascript/firefox-driver/js/firefoxDriver.js b/javascript/firefox-driver/js/firefoxDriver.js
index 8d07bbb..48462b1 100644
--- a/javascript/firefox-driver/js/firefoxDriver.js
+++ b/javascript/firefox-driver/js/firefoxDriver.js
@@ -21,13 +21,13 @@
 goog.require('Utils');
 goog.require('WebLoadingListener');
 goog.require('bot.ErrorCode');
+goog.require('bot.appcache');
+goog.require('bot.connection');
 goog.require('bot.dom');
 goog.require('bot.frame');
 goog.require('bot.locators');
 goog.require('bot.userAgent');
 goog.require('bot.window');
-goog.require('bot.connection');
-goog.require('bot.appcache');
 goog.require('fxdriver.Timer');
 goog.require('fxdriver.events');
 goog.require('fxdriver.io');
@@ -37,9 +37,10 @@
 goog.require('fxdriver.screenshot');
 goog.require('fxdriver.utils');
 goog.require('goog.array');
+goog.require('goog.dom');
 goog.require('goog.dom.selection');
-goog.require('goog.math');
-goog.require('goog.userAgent');
+goog.require('goog.math.Coordinate');
+goog.require('goog.math.Size');
 
 
 FirefoxDriver = function(server, enableNativeEvents, win) {
@@ -56,25 +57,25 @@
   // This really shouldn't be here, but the firefoxdriver isn't compiled with closure, so the atoms
   // aren't exported into global scope
   FirefoxDriver.prototype.dismissAlert.preconditions =
-      [ function() { fxdriver.preconditions.alertPresent(this) } ];
+      [function() { fxdriver.preconditions.alertPresent(this) }];
   FirefoxDriver.prototype.acceptAlert.preconditions =
-      [ function() { fxdriver.preconditions.alertPresent(this) } ];
+      [function() { fxdriver.preconditions.alertPresent(this) }];
   FirefoxDriver.prototype.getAlertText.preconditions =
-      [ function() { fxdriver.preconditions.alertPresent(this) } ];
+      [function() { fxdriver.preconditions.alertPresent(this) }];
   FirefoxDriver.prototype.setAlertValue.preconditions =
-      [ function() { fxdriver.preconditions.alertPresent(this) } ];
+      [function() { fxdriver.preconditions.alertPresent(this) }];
 
 
-  FirefoxDriver.listenerScript = Utils.loadUrl("resource://fxdriver/evaluate.js");
+  FirefoxDriver.listenerScript = Utils.loadUrl('resource://fxdriver/evaluate.js');
 
   this.jsTimer = new fxdriver.Timer();
-  this.mouse = Utils.newInstance("@googlecode.com/webdriver/syntheticmouse;1", "wdIMouse");
+  this.mouse = Utils.newInstance('@googlecode.com/webdriver/syntheticmouse;1', 'wdIMouse');
   // Current state of modifier keys (for synthenized events).
-  this.modifierKeysState = Utils.newInstance("@googlecode.com/webdriver/modifierkeys;1", "wdIModifierKeys");
+  this.modifierKeysState = Utils.newInstance('@googlecode.com/webdriver/modifierkeys;1', 'wdIModifierKeys');
   this.mouse.initialize(this.modifierKeysState);
 
   if (!bot.userAgent.isProductVersion('3.5')) {
-    fxdriver.logging.info("Replacing CSS lookup mechanism with Sizzle");
+    fxdriver.logging.info('Replacing CSS lookup mechanism with Sizzle');
     var cssSelectorFunction = (function() {
       var sizzle = [
         'var originalSizzle = window.Sizzle;',
@@ -123,7 +124,7 @@
                 text == target;
           });
         }
-      }
+      };
     })();
 
 
@@ -137,7 +138,7 @@
 };
 
 
-FirefoxDriver.prototype.__defineGetter__("id", function() {
+FirefoxDriver.prototype.__defineGetter__('id', function() {
   if (!this.id_) {
     this.id_ = this.server.getNextId();
   }
@@ -181,7 +182,7 @@
         respond.sendError(new WebDriverError(bot.ErrorCode.TIMEOUT,
             'Timed out waiting for page load.'));
       } else {
-        respond.value = "";
+        respond.value = '';
         respond.send();
       }
     }, respond.session.getPageLoadTimeout(), respond.session.getWindow());
@@ -190,7 +191,7 @@
   respond.session.getBrowser().loadURI(url);
 
   if (!loadEventExpected) {
-    fxdriver.logging.info("No load event expected");
+    fxdriver.logging.info('No load event expected');
     respond.send();
   }
 };
@@ -199,13 +200,13 @@
 FirefoxDriver.prototype.close = function(respond) {
   // Grab all the references we'll need. Once we call close all this might go away
   var wm = fxdriver.moz.getService(
-      "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator");
+      '@mozilla.org/appshell/window-mediator;1', 'nsIWindowMediator');
   var appService = fxdriver.moz.getService(
-      "@mozilla.org/toolkit/app-startup;1", "nsIAppStartup");
+      '@mozilla.org/toolkit/app-startup;1', 'nsIAppStartup');
   var forceQuit = Components.interfaces.nsIAppStartup.eForceQuit;
 
   var numOpenWindows = 0;
-  var allWindows = wm.getEnumerator("navigator:browser");
+  var allWindows = wm.getEnumerator('navigator:browser');
   while (allWindows.hasMoreElements()) {
     numOpenWindows += 1;
     allWindows.getNext();
@@ -236,7 +237,7 @@
     var browser = respond.session.getBrowser();
     notifyOfCloseWindow(browser.id);
     browser.contentWindow.close();
-  } catch(e) {
+  } catch (e) {
     fxdriver.logging.warning(e);
   }
 
@@ -252,7 +253,7 @@
   var script = parameters['script'];
   var converted = Utils.unwrapParameters(parameters['args'], doc);
 
-  if (doc.designMode && "on" == doc.designMode.toLowerCase()) {
+  if (doc.designMode && 'on' == doc.designMode.toLowerCase()) {
     if (isAsync) {
       respond.sendError(
           'Document designMode is enabled; advanced operations, ' +
@@ -264,7 +265,7 @@
     }
 
     // See https://developer.mozilla.org/en/rich-text_editing_in_mozilla#Internet_Explorer_Differences
-    fxdriver.logging.info("Window in design mode, falling back to sandbox: " + doc.designMode);
+    fxdriver.logging.info('Window in design mode, falling back to sandbox: ' + doc.designMode);
     var window = respond.session.getWindow();
     window = window.wrappedJSObject;
     var sandbox = new Components.utils.Sandbox(window);
@@ -274,8 +275,8 @@
     sandbox.__webdriverParams = converted;
 
     try {
-      var scriptSrc = "with(window) { var __webdriverFunc = function(){" + parameters.script +
-          "};  __webdriverFunc.apply(null, __webdriverParams); }";
+      var scriptSrc = 'with(window) { var __webdriverFunc = function(){' + parameters.script +
+          '};  __webdriverFunc.apply(null, __webdriverParams); }';
       var res = Components.utils.evalInSandbox(scriptSrc, sandbox);
       respond.value = Utils.wrapResult(res, doc);
       respond.send();
@@ -292,7 +293,7 @@
       // The modal detection code in modals.js deals with throwing an
       // exception, in the other case.
       respond.sendError(new WebDriverError(bot.ErrorCode.JAVASCRIPT_ERROR,
-          "waiting for doc.body failed"));
+          'waiting for doc.body failed'));
     }
   };
 
@@ -301,7 +302,7 @@
       // The modal detection code in modals.js deals with throwing an
       // exception, in the other case.
       respond.sendError(new WebDriverError(bot.ErrorCode.JAVASCRIPT_ERROR,
-          "waiting for evaluate.js load failed"));
+          'waiting for evaluate.js load failed'));
     }
   };
 
@@ -345,8 +346,8 @@
   // Attach the listener to the DOM
   var addListener = function() {
     if (!doc.getUserData('webdriver-evaluate-attached')) {
-      var element = doc.createElement("script");
-      element.setAttribute("type", "text/javascript");
+      var element = doc.createElement('script');
+      element.setAttribute('type', 'text/javascript');
       element.innerHTML = FirefoxDriver.listenerScript;
       doc.body.appendChild(element);
       element.parentNode.removeChild(element);
@@ -359,7 +360,7 @@
   };
 
   timer.runWhenTrue(checkDocBodyLoaded, addListener, 10000, docBodyLoadTimeOut);
-};
+}
 
 
 FirefoxDriver.prototype.executeScript = function(respond, parameters) {
@@ -384,7 +385,7 @@
   var docElement = win.document.documentElement;
   if (!docElement) {
     // empty string means no DOM element available (the page is probably rebuilding at the moment)
-    respond.value = "";
+    respond.value = '';
     respond.send();
     return;
   }
@@ -413,7 +414,7 @@
  * @private
  */
 FirefoxDriver.annotateInvalidSelectorError_ = function(selector, ex) {
-  if(ex.code == bot.ErrorCode.INVALID_SELECTOR_ERROR) {
+  if (ex.code == bot.ErrorCode.INVALID_SELECTOR_ERROR) {
     return new WebDriverError(bot.ErrorCode.INVALID_SELECTOR_ERROR,
         'The given selector ' + selector +
             ' is either invalid or does not result' +
@@ -422,8 +423,8 @@
 
   try {
     var converted = ex.QueryInterface(Components.interfaces['nsIException']);
-    fxdriver.logging.info("Converted the exception: " + converted.name);
-    if ("NS_ERROR_DOM_SYNTAX_ERR" == converted.name) {
+    fxdriver.logging.info('Converted the exception: ' + converted.name);
+    if ('NS_ERROR_DOM_SYNTAX_ERR' == converted.name) {
       return new WebDriverError(bot.ErrorCode.INVALID_SELECTOR_ERROR,
           'The given selector ' + selector +
               ' is either invalid or does not result' +
@@ -617,26 +618,26 @@
   var switchingToDefault = !goog.isDef(parameters.id) || goog.isNull(parameters.id);
   if ((!currentWindow || currentWindow.closed) && !switchingToDefault) {
     // By definition there will be no child frames.
-    respond.sendError(new WebDriverError(bot.ErrorCode.NO_SUCH_FRAME, "Current window is closed"));
+    respond.sendError(new WebDriverError(bot.ErrorCode.NO_SUCH_FRAME, 'Current window is closed'));
   }
 
   var newWindow = null;
   if (switchingToDefault) {
-    fxdriver.logging.info("Switching to default content (topmost frame)");
+    fxdriver.logging.info('Switching to default content (topmost frame)');
     newWindow = respond.session.getBrowser().contentWindow;
   } else if (goog.isString(parameters.id)) {
-    fxdriver.logging.info("Switching to frame with name or ID: " + parameters.id);
+    fxdriver.logging.info('Switching to frame with name or ID: ' + parameters.id);
     newWindow = bot.frame.findFrameByNameOrId(parameters.id, currentWindow);
   } else if (goog.isNumber(parameters.id)) {
-    fxdriver.logging.info("Switching to frame by index: " + parameters.id);
+    fxdriver.logging.info('Switching to frame by index: ' + parameters.id);
     newWindow = bot.frame.findFrameByIndex(parameters.id, currentWindow);
   } else if (goog.isObject(parameters.id) && 'ELEMENT' in parameters.id) {
-    fxdriver.logging.info("Switching to frame by element: " + parameters.id['ELEMENT']);
+    fxdriver.logging.info('Switching to frame by element: ' + parameters.id['ELEMENT']);
 
     var element = Utils.getElementAt(parameters.id['ELEMENT'],
         currentWindow.document);
 
-    element = fxdriver.moz.unwrapFor4(element)
+    element = fxdriver.moz.unwrapFor4(element);
 
     if (/^i?frame$/i.test(element.tagName)) {
       // Each session maintains a weak reference to the window it is currently
@@ -667,7 +668,7 @@
   var element = Utils.getActiveElement(respond.session.getDocument());
   var id = Utils.addToKnownElements(element);
 
-  respond.value = {'ELEMENT':id};
+  respond.value = {'ELEMENT': id};
   respond.send();
 };
 
@@ -728,7 +729,7 @@
     var currDomain = currLocation.host;
     if (currDomain.indexOf(cookie.domain) == -1) {  // Not quite right, but close enough
       throw new WebDriverError(bot.ErrorCode.INVALID_COOKIE_DOMAIN,
-          "You may only set cookies for the current domain");
+          'You may only set cookies for the current domain');
     }
   }
 
@@ -736,17 +737,17 @@
   // We'll catch ip6 addresses by mistake. Since no-one uses those
   // this will be okay for now.
   if (cookie.domain.match(/:\d+$/)) {
-    cookie.domain = cookie.domain.replace(/:\d+$/, "");
+    cookie.domain = cookie.domain.replace(/:\d+$/, '');
   }
 
   var document = respond.session.getDocument();
   if (!document || !document.contentType.match(/html/i)) {
     throw new WebDriverError(bot.ErrorCode.UNABLE_TO_SET_COOKIE,
-        "You may only set cookies on html documents");
+        'You may only set cookies on html documents');
   }
 
   var cookieManager =
-      fxdriver.moz.getService("@mozilla.org/cookiemanager;1", "nsICookieManager2");
+      fxdriver.moz.getService('@mozilla.org/cookiemanager;1', 'nsICookieManager2');
 
   // The signature for "add" is different in firefox 3 and 2. We should sniff
   // the browser version and call the right version of the method, but for now
@@ -754,7 +755,7 @@
   try {
     cookieManager.add(cookie.domain, cookie.path, cookie.name, cookie.value,
         cookie.secure, false, cookie.expiry);
-  } catch(e) {
+  } catch (e) {
     cookieManager.add(cookie.domain, cookie.path, cookie.name, cookie.value,
         cookie.secure, false, false, cookie.expiry);
   }
@@ -766,25 +767,25 @@
   var results = [];
 
   var currentPath = location.pathname;
-  if (!currentPath) currentPath = "/";
+  if (!currentPath) currentPath = '/';
   var isForCurrentPath = function(aPath) {
     return currentPath.indexOf(aPath) != -1;
   };
-  var cm = fxdriver.moz.getService("@mozilla.org/cookiemanager;1", "nsICookieManager");
+  var cm = fxdriver.moz.getService('@mozilla.org/cookiemanager;1', 'nsICookieManager');
   var e = cm.enumerator;
   while (e.hasMoreElements()) {
-    var cookie = e.getNext().QueryInterface(Components.interfaces["nsICookie"]);
+    var cookie = e.getNext().QueryInterface(Components.interfaces['nsICookie']);
 
     // Take the hostname and progressively shorten it
     var hostname = location.hostname;
     do {
-      if ((cookie.host == "." + hostname || cookie.host == hostname)
+      if ((cookie.host == '.' + hostname || cookie.host == hostname)
           && isForCurrentPath(cookie.path)) {
         results.push(cookie);
         break;
       }
-      hostname = hostname.replace(/^.*?\./, "");
-    } while (hostname.indexOf(".") != -1);
+      hostname = hostname.replace(/^.*?\./, '');
+    } while (hostname.indexOf('.') != -1);
   }
 
   return results;
@@ -821,7 +822,7 @@
 // doesn't always do The Right Thing
 FirefoxDriver.prototype.deleteCookie = function(respond, parameters) {
   var toDelete = parameters.name;
-  var cm = fxdriver.moz.getService("@mozilla.org/cookiemanager;1", "nsICookieManager");
+  var cm = fxdriver.moz.getService('@mozilla.org/cookiemanager;1', 'nsICookieManager');
 
   var cookies = getVisibleCookies(respond.session.getBrowser().
       contentWindow.location);
@@ -837,7 +838,7 @@
 
 
 FirefoxDriver.prototype.deleteAllCookies = function(respond) {
-  var cm = fxdriver.moz.getService("@mozilla.org/cookiemanager;1", "nsICookieManager");
+  var cm = fxdriver.moz.getService('@mozilla.org/cookiemanager;1', 'nsICookieManager');
   var cookies = getVisibleCookies(respond.session.getBrowser().
       contentWindow.location);
 
@@ -889,11 +890,11 @@
     var canvas = fxdriver.screenshot.grab(window);
     try {
       fxdriver.screenshot.save(canvas, pngFile);
-    } catch(e) {
+    } catch (e) {
       throw new WebDriverError(bot.ErrorCode.UNKNOWN_ERROR,
           'Could not save screenshot to ' + pngFile + ' - ' + e);
     }
-  } catch(e) {
+  } catch (e) {
     throw new WebDriverError(bot.ErrorCode.UNKNOWN_ERROR,
         'Could not take screenshot of current page - ' + e);
   }
@@ -942,7 +943,7 @@
   fxdriver.modals.isModalPresent(
     function(present) {
       if (present) {
-        respond.value = fxdriver.modals.getText(driver)
+        respond.value = fxdriver.modals.getText(driver);
       } else {
         respond.status = bot.ErrorCode.NO_MODAL_DIALOG_OPEN;
         respond.value = { message: 'No alert is present' };
@@ -970,7 +971,7 @@
     respond.value = returnArray;
   } catch (e) {
     throw new WebDriverError(bot.ErrorCode.IME_NOT_AVAILABLE,
-        "IME not available on the host: " + e);
+        'IME not available on the host: ' + e);
   }
   respond.send();
 };
@@ -983,7 +984,7 @@
     respond.value = activeEngine.value;
   } catch (e) {
     throw new WebDriverError(bot.ErrorCode.IME_NOT_AVAILABLE,
-        "IME not available on the host: " + e);
+        'IME not available on the host: ' + e);
   }
   respond.send();
 };
@@ -996,7 +997,7 @@
     respond.value = isActive.value;
   } catch (e) {
     throw new WebDriverError(bot.ErrorCode.IME_NOT_AVAILABLE,
-        "IME not available on the host: " + e);
+        'IME not available on the host: ' + e);
   }
   respond.send();
 };
@@ -1007,7 +1008,7 @@
     obj.imeDeactivate();
   } catch (e) {
     throw new WebDriverError(bot.ErrorCode.IME_NOT_AVAILABLE,
-        "IME not available on the host: " + e);
+        'IME not available on the host: ' + e);
   }
 
   respond.send();
@@ -1021,12 +1022,12 @@
     obj.imeActivateEngine(engineToActivate, successfulActivation);
   } catch (e) {
     throw new WebDriverError(bot.ErrorCode.IME_NOT_AVAILABLE,
-        "IME not available on the host: " + e);
+        'IME not available on the host: ' + e);
   }
 
   if (! successfulActivation.value) {
     throw new WebDriverError(bot.ErrorCode.IME_ENGINE_ACTIVATION_FAILED,
-        "Activation of engine failed: " + engineToActivate);
+        'Activation of engine failed: ' + engineToActivate);
   }
   respond.send();
 };
@@ -1050,28 +1051,28 @@
 
   if (mouseLocation.initialized) {
     elementForNode = doc.elementFromPoint(locationX, locationY);
-    fxdriver.logging.info("Element from (" + locationX + "," + locationY + ") :" + elementForNode);
+    fxdriver.logging.info('Element from (' + locationX + ',' + locationY + ') :' + elementForNode);
   } else {
-    fxdriver.logging.info("Mouse coordinates were not set - using body");
-    elementForNode = doc.getElementsByTagName("body")[0];
+    fxdriver.logging.info('Mouse coordinates were not set - using body');
+    elementForNode = doc.getElementsByTagName('body')[0];
   }
 
   return fxdriver.moz.unwrap(elementForNode);
 }
 
 function generateErrorForNativeEvents(nativeEventsEnabled, nativeEventsObj, nodeForInteraction) {
-  var nativeEventFailureCause = "Could not get node for element or native " +
-      "events are not supported on the platform.";
+  var nativeEventFailureCause = 'Could not get node for element or native ' +
+      'events are not supported on the platform.';
   if (!nativeEventsEnabled) {
-    nativeEventFailureCause = "native events are disabled on this platform.";
+    nativeEventFailureCause = 'native events are disabled on this platform.';
   } else if (!nativeEventsObj) {
-    nativeEventFailureCause = "Could not load native events component.";
+    nativeEventFailureCause = 'Could not load native events component.';
   } else {
-    nativeEventFailureCause = "Could not get node for element - cannot interact.";
+    nativeEventFailureCause = 'Could not get node for element - cannot interact.';
   }
  // TODO: use the correct error type here.
   return new WebDriverError(bot.ErrorCode.INVALID_ELEMENT_STATE,
-      "Cannot perform native interaction: " + nativeEventFailureCause);
+      'Cannot perform native interaction: ' + nativeEventFailureCause);
 }
 
 FirefoxDriver.prototype.sendResponseFromSyntheticMouse_ = function(mouseReturnValue, respond) {
@@ -1102,7 +1103,7 @@
   // Fast path first
   if (!this.enableNativeEvents) {
     var target = parameters['element'] ? Utils.getElementAt(parameters['element'], doc) : null;
-    fxdriver.logging.info("Calling move with: " + parameters['xoffset'] + ', ' + parameters['yoffset'] + ", " + target);
+    fxdriver.logging.info('Calling move with: ' + parameters['xoffset'] + ', ' + parameters['yoffset'] + ', ' + target);
     var result = this.mouse.move(target, parameters['xoffset'], parameters['yoffset']);
     this.sendResponseFromSyntheticMouse_(result, respond);
 
@@ -1134,7 +1135,7 @@
     } catch (ex) {
       if (ex.code == bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS) {
         respond.sendError(new WebDriverError(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS,
-          "Given coordinates (" + clickPoint_ownerDocumentPreScroll.x + ", " + clickPoint_ownerDocumentPreScroll.y + ") are outside the document. Error: " + ex));
+          'Given coordinates (' + clickPoint_ownerDocumentPreScroll.x + ', ' + clickPoint_ownerDocumentPreScroll.y + ') are outside the document. Error: ' + ex));
         return;
       }
       else {
@@ -1154,8 +1155,8 @@
     if (nativeEventsEnabled && nativeMouse && node) {
       var currentPosition = respond.session.getMousePosition();
       var currentPosition_windowHandle = {x: currentPosition.x + browserOffset.x, y: currentPosition.y + browserOffset.y};
-      fxdriver.logging.info("Moving from (" + currentPosition.x + ", " + currentPosition.y + ") to (" +
-        clickPoint_ownerDocumentPostScroll.x + ", " + clickPoint_ownerDocumentPostScroll.y + ")");
+      fxdriver.logging.info('Moving from (' + currentPosition.x + ', ' + currentPosition.y + ') to (' +
+        clickPoint_ownerDocumentPostScroll.x + ', ' + clickPoint_ownerDocumentPostScroll.y + ')');
       nativeMouse.mouseMove(node,
           currentPosition_windowHandle.x, currentPosition_windowHandle.y,
           mouseTarget_ownerDocument_windowHandle.x, mouseTarget_ownerDocument_windowHandle.y);
@@ -1241,8 +1242,8 @@
     if (isMouseButtonPressed) {
       upX = currentPosition.viewPortXOffset;
       upY = currentPosition.viewPortYOffset;
-      fxdriver.logging.info("Button pressed. Using coordiantes with viewport offset: "
-          + upX + ", " + upY);
+      fxdriver.logging.info('Button pressed. Using coordiantes with viewport offset: '
+          + upX + ', ' + upY);
     }
     var browserOffset = Utils.getBrowserSpecificOffset(respond.session.getBrowser());
 
@@ -1274,7 +1275,7 @@
     // The right mouse button is defined as '2' in the wire protocol
     var RIGHT_MOUSE_BUTTON = 2;
     var result;
-    if(RIGHT_MOUSE_BUTTON == button) {
+    if (RIGHT_MOUSE_BUTTON == button) {
       result = this.mouse.contextClick(null);
     } else {
       result = this.mouse.click(null);
@@ -1353,19 +1354,19 @@
 
   var currentlyActiveElement = Utils.getActiveElement(respond.session.getDocument());
 
-  if(bot.dom.isEditable(currentlyActiveElement)) {
+  if (bot.dom.isEditable(currentlyActiveElement)) {
       goog.dom.selection.setCursorPosition(
           currentlyActiveElement, currentlyActiveElement.value.length);
   }
 
   var useElement = currentlyActiveElement;
   var tagName = useElement.tagName.toLowerCase();
-  if (tagName == "body" && useElement.ownerDocument.defaultView.frameElement) {
+  if (tagName == 'body' && useElement.ownerDocument.defaultView.frameElement) {
     useElement.ownerDocument.defaultView.focus();
 
     // Turns out, this is what we should be using as the target
     // to send events to
-    useElement = useElement.ownerDocument.getElementsByTagName("html")[0];
+    useElement = useElement.ownerDocument.getElementsByTagName('html')[0];
   }
 
   // In case Utils.type performs non-native typing, it will modify the state of the
@@ -1422,32 +1423,32 @@
 
   var documentWindow = respond.session.getWindow();
   var chromeWindow = this.getChromeWindowFromDocumentWindow(documentWindow);
-  
+
   chromeWindow.maximize();
 
   respond.send();
 };
 
 
-FirefoxDriver.prototype.getChromeWindowFromDocumentWindow = function(documentWindow){
-  // Find the chrome window for the requested document window. 
+FirefoxDriver.prototype.getChromeWindowFromDocumentWindow = function(documentWindow) {
+  // Find the chrome window for the requested document window.
   // This will ignore unfocused tabs
   var wm = fxdriver.moz.getService(
-    "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator");
-  var allWindows = wm.getEnumerator("navigator:browser");
+    '@mozilla.org/appshell/window-mediator;1', 'nsIWindowMediator');
+  var allWindows = wm.getEnumerator('navigator:browser');
 
   while (allWindows.hasMoreElements()) {
-    var chromeWindow = allWindows.getNext()	  
+    var chromeWindow = allWindows.getNext();
 
     if (chromeWindow.gBrowser.contentWindow == documentWindow.top) {
       return chromeWindow;
     }
-  }  
+  }
 };
 
 //TODO(jari): could this be made into a precondition?
 FirefoxDriver.prototype.assertTargetsCurrentWindow_ = function(parameters) {
-  if (parameters.windowHandle != "current") {
+  if (parameters.windowHandle != 'current') {
     throw new WebDriverError(bot.ErrorCode.UNSUPPORTED_OPERATION,
       'Window operations are only supported for the currently focused window.');
   }
diff --git a/javascript/firefox-driver/js/logger.js b/javascript/firefox-driver/js/logger.js
index a7c6895..b71f253 100644
--- a/javascript/firefox-driver/js/logger.js
+++ b/javascript/firefox-driver/js/logger.js
@@ -21,17 +21,17 @@
 goog.require('fxdriver.prefs');
 goog.require('goog.array');
 goog.require('goog.debug.Formatter');
-goog.require('goog.debug.LogRecord');
 goog.require('goog.debug.Logger');
 goog.require('goog.debug.TextFormatter');
 goog.require('goog.object');
+goog.require('goog.string');
 
 
 
 /**
  * Represents the logging preferences as sent across the wire.
  *
- * @typedef {{driver: (number=), profiler: (number=)}}
+ * @typedef {{driver: (number|undefined), profiler: (number|undefined)}}
  */
 fxdriver.logging.LoggingPreferences;
 
@@ -736,23 +736,23 @@
 /**
  * Takes an object and attempts to discover which interfaces it implements.
  *
- * @param {*} object The object to dump
+ * @param {*} object The object to dump.
  */
 fxdriver.logging.dumpObject = function(element) {
-  var msg = "=============\n";
+  var msg = '=============\n';
 
   var rows = [];
 
-  msg += "Supported interfaces: ";
+  msg += 'Supported interfaces: ';
   for (var i in Components.interfaces) {
     try {
       var view = element.QueryInterface(Components.interfaces[i]);
-      msg += i + ", ";
+      msg += i + ', ';
     } catch (e) {
       // Doesn't support the interface
     }
   }
-  msg += "\n------------\n";
+  msg += '\n------------\n';
 
   try {
     fxdriver.logging.dumpProperties_(element, rows);
@@ -761,29 +761,29 @@
 
   rows.sort();
   for (var j in rows) {
-    msg += rows[j] + "\n";
+    msg += rows[j] + '\n';
   }
 
-  msg += "=============\n\n\n";
+  msg += '=============\n\n\n';
   fxdriver.logging.info(msg);
 };
 
 /**
- * @param {*} view The object to get the properties of
+ * @param {*} view The object to get the properties of.
  * @param {!Array.<string>} rows The place to output results to.
  * @private
  */
 fxdriver.logging.dumpProperties_ = function(view, rows) {
   for (var i in view) {
-    var value = "\t" + i + ": ";
+    var value = '\t' + i + ': ';
     try {
       if (typeof(view[i]) == typeof(Function)) {
-        value += " function()";
+        value += ' function()';
       } else {
         value += String(view[i]);
       }
     } catch (e) {
-      value += " Cannot obtain value";
+      value += ' Cannot obtain value';
     }
     rows.push(value);
   }
diff --git a/javascript/firefox-driver/js/modals.js b/javascript/firefox-driver/js/modals.js
index cbf7949..5f80bd5 100644
--- a/javascript/firefox-driver/js/modals.js
+++ b/javascript/firefox-driver/js/modals.js
@@ -16,15 +16,15 @@
  */
 
 /**
- * @fileoverview Methods for dealing with modal dialogs
+ * @fileoverview Methods for dealing with modal dialogs.
  */
 
 goog.provide('fxdriver.modals');
 
 goog.require('WebDriverError');
 goog.require('bot.ErrorCode');
-goog.require('fxdriver.logging');
 goog.require('fxdriver.Timer');
+goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
 goog.require('fxdriver.utils');
 
@@ -43,7 +43,7 @@
 
 fxdriver.modals.acceptAlert = function(driver) {
   var modal = fxdriver.modals.find_();
-  var button = fxdriver.modals.findButton_(modal, "accept");
+  var button = fxdriver.modals.findButton_(modal, 'accept');
   button.click();
   fxdriver.modals.clearFlag_(driver);
 };
@@ -51,11 +51,11 @@
 
 fxdriver.modals.dismissAlert = function(driver) {
   var modal = fxdriver.modals.find_();
-  var button = fxdriver.modals.findButton_(modal, "cancel");
+  var button = fxdriver.modals.findButton_(modal, 'cancel');
 
   if (!button) {
     fxdriver.logging.info('No cancel button Falling back to the accept button');
-    button = fxdriver.modals.findButton_(modal, "accept");
+    button = fxdriver.modals.findButton_(modal, 'accept');
   }
 
   button.click();
@@ -110,7 +110,7 @@
 
 fxdriver.modals.findButton_ = function(modal, value) {
   var doc = modal.document;
-  var dialog = doc.getElementsByTagName("dialog")[0];
+  var dialog = doc.getElementsByTagName('dialog')[0];
   return dialog.getButton(value);
 };
 
@@ -126,7 +126,7 @@
 
 
 fxdriver.modals.findAssociatedDriver_ = function(window) {
-  var ww = CC["@mozilla.org/embedcomp/window-watcher;1"].getService(CI["nsIWindowWatcher"]);
+  var ww = CC['@mozilla.org/embedcomp/window-watcher;1'].getService(CI['nsIWindowWatcher']);
 
   var parent = window ? window : ww.activeWindow;
   if (parent.wrappedJSObject) {
@@ -135,13 +135,13 @@
   var top = parent.top;
 
   // Now iterate over all open browsers to find the one we belong to
-  var wm = CC["@mozilla.org/appshell/window-mediator;1"].getService(CI["nsIWindowMediator"]);
-  var allWindows = wm.getEnumerator("navigator:browser");
+  var wm = CC['@mozilla.org/appshell/window-mediator;1'].getService(CI['nsIWindowMediator']);
+  var allWindows = wm.getEnumerator('navigator:browser');
   while (allWindows.hasMoreElements()) {
     var chrome = allWindows.getNext().QueryInterface(CI.nsIDOMWindow);
     if (chrome.content == window) {
       return chrome.fxdriver;
-    } else if(chrome.content.top == window.top) {
+    } else if (chrome.content.top == window.top) {
       return chrome.fxdriver;
     }
   }
@@ -152,7 +152,7 @@
 };
 
 fxdriver.modals.signalOpenModal = function(parent, text) {
-  fxdriver.logging.info("signalOpenModal");
+  fxdriver.logging.info('signalOpenModal');
   // Try to grab the top level window
   var driver = fxdriver.modals.findAssociatedDriver_(parent);
   if (driver && driver.response_) {
@@ -211,7 +211,7 @@
   }
 
   return 'dismiss';
-}
+};
 
 
 /**
@@ -221,10 +221,10 @@
  */
 fxdriver.modals.configure = function(unexpectedAlertBehaviour) {
   var prefs = fxdriver.moz.getService(
-      "@mozilla.org/preferences-service;1", "nsIPrefBranch");
+      '@mozilla.org/preferences-service;1', 'nsIPrefBranch');
 
   var value = fxdriver.modals.asAcceptableAlertValue(unexpectedAlertBehaviour);
-  prefs.setCharPref("webdriver_unexpected_alert_behaviour", value);
+  prefs.setCharPref('webdriver_unexpected_alert_behaviour', value);
 };
 
 
@@ -233,12 +233,12 @@
  */
 fxdriver.modals.getUnexpectedAlertBehaviour = function() {
   var prefs = fxdriver.moz.getService(
-      "@mozilla.org/preferences-service;1", "nsIPrefBranch");
+      '@mozilla.org/preferences-service;1', 'nsIPrefBranch');
 
-  if (!prefs.prefHasUserValue("webdriver_unexpected_alert_behaviour")) {
+  if (!prefs.prefHasUserValue('webdriver_unexpected_alert_behaviour')) {
     return 'dismiss';
   }
 
-  var raw = prefs.getCharPref("webdriver_unexpected_alert_behaviour");
+  var raw = prefs.getCharPref('webdriver_unexpected_alert_behaviour');
   return fxdriver.modals.asAcceptableAlertValue(raw);
 };
diff --git a/javascript/firefox-driver/js/modifierKeys.js b/javascript/firefox-driver/js/modifierKeys.js
index 0caecb5..203c23e 100644
--- a/javascript/firefox-driver/js/modifierKeys.js
+++ b/javascript/firefox-driver/js/modifierKeys.js
@@ -1,51 +1,50 @@
 goog.provide('ModifierKeys');
 
+goog.require('Utils');
 goog.require('bot.Device');
 goog.require('fxdriver.moz');
-goog.require('goog.userAgent');
-goog.require('Utils');
 
-ModifierKeys = function () {
+ModifierKeys = function() {
   this.wrappedJSObject = this;
 
-  this.QueryInterface = fxdriver.moz.queryInterface (this, [CI.nsISupports, CI.wdIModifierKeys]);
+  this.QueryInterface = fxdriver.moz.queryInterface(this, [CI.nsISupports, CI.wdIModifierKeys]);
   this.backingState_ = new bot.Device.ModifiersState();
 };
 
 ModifierKeys.prototype.isShiftPressed = function() {
   return this.backingState_.isShiftPressed();
-}
+};
 
 ModifierKeys.prototype.isControlPressed = function() {
   return this.backingState_.isControlPressed();
-}
+};
 
 ModifierKeys.prototype.isAltPressed = function() {
   return this.backingState_.isAltPressed();
-}
+};
 
 ModifierKeys.prototype.isMetaPressed = function() {
   return this.backingState_.isMetaPressed();
-}
+};
 
 ModifierKeys.prototype.setShiftPressed = function(isPressed) {
   this.backingState_.setPressed(bot.Device.Modifier.SHIFT, isPressed);
-}
+};
 
 ModifierKeys.prototype.setControlPressed = function(isPressed) {
   this.backingState_.setPressed(bot.Device.Modifier.CONTROL, isPressed);
-}
+};
 
 ModifierKeys.prototype.setAltPressed = function(isPressed) {
   this.backingState_.setPressed(bot.Device.Modifier.ALT, isPressed);
-}
+};
 ModifierKeys.prototype.setMetaPressed = function(isPressed) {
   this.backingState_.setPressed(bot.Device.Modifier.META, isPressed);
-}
+};
 
 ModifierKeys.prototype.classDescription = 'Keeps the state of the modifier keys (shift, alt, meta, ctrl)';
 ModifierKeys.prototype.contractID = '@googlecode.com/webdriver/modifierkeys;1';
-ModifierKeys.prototype.classID = Components.ID ('{2E4B69B9-21FE-48ad-A2F6-AB355D6D2FCE}');
+ModifierKeys.prototype.classID = Components.ID('{2E4B69B9-21FE-48ad-A2F6-AB355D6D2FCE}');
 
 /** @const */ var components = [ModifierKeys];
 
@@ -55,4 +54,4 @@
   NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
 } else {
   NSGetModule = XPCOMUtils.generateNSGetModule(components);
-}
\ No newline at end of file
+}
diff --git a/javascript/firefox-driver/js/moz.js b/javascript/firefox-driver/js/moz.js
index 0d4a079..4757183 100644
--- a/javascript/firefox-driver/js/moz.js
+++ b/javascript/firefox-driver/js/moz.js
@@ -19,6 +19,7 @@
 
 goog.require('bot.userAgent');
 goog.require('fxdriver.logging');
+goog.require('goog.array');
 
 
 /** @const */ var CC = Components.classes;
@@ -93,7 +94,7 @@
  * Unwraps a something which is wrapped into a XPCNativeWrapper or XrayWrapper.
  *
  * @param {!Object} thing The "something" to unwrap.
- * @returns {!Object} The object, unwrapped if possible.
+ * @return {!Object} The object, unwrapped if possible.
  */
 fxdriver.moz.unwrap = function(thing) {
   // TODO(simon): This is identical to the same function in firefox-chrome
@@ -121,7 +122,7 @@
       toReturn.__fxdriver_unwrapped = true;
       return toReturn;
     }
-  } catch(e) {
+  } catch (e) {
     // Unwrapping will fail for JS literals - numbers, for example. Catch
     // the exception and proceed, it will eventually be returned as-is.
   }
@@ -136,13 +137,13 @@
  * See: https://developer.mozilla.org/en/XPCNativeWrapper
  */
 fxdriver.moz.unwrapXpcOnly = function(thing) {
-  if (XPCNativeWrapper && "unwrap" in XPCNativeWrapper) {
+  if (XPCNativeWrapper && 'unwrap' in XPCNativeWrapper) {
     try {
       return XPCNativeWrapper.unwrap(thing);
-    } catch(e) {
+    } catch (e) {
       //Unwrapping will fail for JS literals - numbers, for example. Catch
       // the exception and proceed, it will eventually be returend as-is.
-      fxdriver.logging.warning("Unwrap From XPC only failed: " + e);
+      fxdriver.logging.warning('Unwrap From XPC only failed: ' + e);
     }
 
   }
@@ -157,4 +158,3 @@
   }
   return doc;
 };
-
diff --git a/javascript/firefox-driver/js/nsCommandProcessor.js b/javascript/firefox-driver/js/nsCommandProcessor.js
index 1c6bff2..92a6606 100644
--- a/javascript/firefox-driver/js/nsCommandProcessor.js
+++ b/javascript/firefox-driver/js/nsCommandProcessor.js
@@ -29,14 +29,13 @@
 goog.require('bot.ErrorCode');
 goog.require('bot.locators');
 goog.require('bot.userAgent');
-goog.require('fxdriver.logging');
 goog.require('fxdriver.Timer');
 goog.require('fxdriver.error');
-goog.require('fxdriver.moz');
+goog.require('fxdriver.logging');
 goog.require('fxdriver.modals');
+goog.require('fxdriver.moz');
 goog.require('fxdriver.profiler');
-goog.require('goog.dom');
-goog.require('goog.object');
+goog.require('goog.array');
 goog.require('wdSessionStoreService');
 
 
@@ -79,9 +78,9 @@
    * @param {window} win The content window that the command will be executed on.
    */
   startCommand: function(win) {
-    this.statusBarLabel_ = win.document.getElementById("fxdriver-label");
+    this.statusBarLabel_ = win.document.getElementById('fxdriver-label');
     if (this.statusBarLabel_) {
-      this.statusBarLabel_.style.color = "red";
+      this.statusBarLabel_.style.color = 'red';
     }
   },
 
@@ -119,11 +118,11 @@
   },
 
   set name(name) { this.json_.name = name; },
-  get name()     { return this.json_.name; },
+  get name() { return this.json_.name; },
   set status(newStatus) { this.json_.status = newStatus; },
-  get status()          { return this.json_.status; },
-  set value(val)     { this.json_.value = val; },
-  get value()        { return this.json_.value; }
+  get status() { return this.json_.status; },
+  set value(val) { this.json_.value = val; },
+  get value() { return this.json_.value; }
 };
 
 
@@ -214,7 +213,7 @@
       var rawRequest = requests.getNext();
 
       try {
-        request = rawRequest.QueryInterface(Components.interfaces.nsIRequest)
+        request = rawRequest.QueryInterface(Components.interfaces.nsIRequest);
       } catch (e) {
         // This may happen for pages that use WebSockets.
         // See https://bugzilla.mozilla.org/show_bug.cgi?id=765618
@@ -226,7 +225,7 @@
       var isPending = false;
       try {
         isPending = request.isPending();
-      } catch(e) {
+      } catch (e) {
           // Normal during page load, which means we should just return "true"
         return true;
       }
@@ -313,7 +312,7 @@
           this.response_, parameters);
       var guards = goog.bind(this.checkPreconditions_, this,
           driverFunction.preconditions, this.response_, parameters);
-      
+
       var toExecute = function() {
         try {
           guards();
@@ -334,7 +333,7 @@
       toExecute();
     } catch (e) {
       if (!e.isWebDriverError) {
-        fxdriver.logging.error('Exception caught by driver: ' + 
+        fxdriver.logging.error('Exception caught by driver: ' +
           this.command_.name + '(' + this.command_.parameters + ')');
         fxdriver.logging.error(e);
       }
@@ -356,7 +355,7 @@
     eval(Utils.loadUrl('resource://fxdriver/json2.js'));
   }
 
-  var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces["nsIPrefBranch"]);
+  var prefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces['nsIPrefBranch']);
 
   if (prefs.prefHasUserValue('webdriver.load.strategy')) {
     loadStrategy_ = prefs.getCharPref('webdriver.load.strategy');
@@ -469,7 +468,7 @@
   }
 
   if (driver.modalOpen) {
-    if (command.name != 'getAlertText' && 
+    if (command.name != 'getAlertText' &&
         command.name != 'setAlertValue' &&
         command.name != 'acceptAlert' &&
         command.name != 'dismissAlert') {
@@ -660,7 +659,7 @@
           // See https://developer.mozilla.org/en/XPCOM_ABI
           return (xulRuntime.XPCOMABI || 'unknown').split('-')[0];
         } catch (ignored) {
-          return 'unknown'
+          return 'unknown';
         }
       })(),
       // See https://developer.mozilla.org/en/OS_TARGET
@@ -683,7 +682,7 @@
  * @param {Response} response The object to send the command response in.
  */
 nsCommandProcessor.prototype.newSession = function(response, parameters) {
-  var win = this.wm.getMostRecentWindow("navigator:browser");
+  var win = this.wm.getMostRecentWindow('navigator:browser');
   var driver = win.fxdriver;
   if (!driver) {
     response.sendError(new WebDriverError(bot.ErrorCode.UNKNOWN_ERROR,
@@ -789,7 +788,7 @@
 };
 
 
-nsCommandProcessor.prototype.QueryInterface = function (aIID) {
+nsCommandProcessor.prototype.QueryInterface = function(aIID) {
   if (!aIID.equals(Components.interfaces.nsICommandProcessor) &&
       !aIID.equals(Components.interfaces.nsISupports)) {
     throw Components.results.NS_ERROR_NO_INTERFACE;
@@ -810,7 +809,7 @@
  * {@code CommandProcessor}.
  */
 nsCommandProcessor.Factory = {
-  instance_ : null,
+  instance_: null,
 
   createInstance: function(aOuter, aIID) {
     if (aOuter != null) {
@@ -871,7 +870,7 @@
 };
 
 nsCommandProcessor.prototype.classID = nsCommandProcessor.CLASS_ID;
-fxdriver.moz.load("resource://gre/modules/XPCOMUtils.jsm");
+fxdriver.moz.load('resource://gre/modules/XPCOMUtils.jsm');
 if (XPCOMUtils.generateNSGetFactory) {
   /** @const */ NSGetFactory = XPCOMUtils.generateNSGetFactory([nsCommandProcessor]);
 }
diff --git a/javascript/firefox-driver/js/preconditions.js b/javascript/firefox-driver/js/preconditions.js
index 52502cc..f56fc1b 100644
--- a/javascript/firefox-driver/js/preconditions.js
+++ b/javascript/firefox-driver/js/preconditions.js
@@ -20,8 +20,8 @@
 
 goog.provide('fxdriver.preconditions');
 
-goog.require('bot.dom');
 goog.require('Utils');
+goog.require('bot.dom');
 
 
 /**
diff --git a/javascript/firefox-driver/js/profiler.js b/javascript/firefox-driver/js/profiler.js
index 13f7e83..b132b5e 100644
--- a/javascript/firefox-driver/js/profiler.js
+++ b/javascript/firefox-driver/js/profiler.js
@@ -27,4 +27,3 @@
   fxdriver.logging.log(fxdriver.logging.LogType.PROFILER,
       fxdriver.logging.LogLevel.INFO, message);
 };
-
diff --git a/javascript/firefox-driver/js/promptService.js b/javascript/firefox-driver/js/promptService.js
index 6064d0b..06c0857 100644
--- a/javascript/firefox-driver/js/promptService.js
+++ b/javascript/firefox-driver/js/promptService.js
@@ -26,7 +26,7 @@
     try {
       var queried = delegate.QueryInterface(iid);
       for (var i in queried) {
-        if (!goog.array.contains(EXCLUDED_NAMES,  i.toString())) {
+        if (!goog.array.contains(EXCLUDED_NAMES, i.toString())) {
           to[i] = queried[i];
         }
       }
@@ -53,7 +53,7 @@
 
 ObservingAlert.prototype.alertCheck = function(dialogTitle, text, checkMsg, checkValue) {
   fxdriver.modals.signalOpenModal(this.parentWindow_, text);
-  this.delegate_.alertCheck(dialogTitle, text, checkMsg,  checkValue);
+  this.delegate_.alertCheck(dialogTitle, text, checkMsg, checkValue);
 };
 
 ObservingAlert.prototype.confirm = function(dialogTitle, text) {
@@ -63,7 +63,7 @@
 
 ObservingAlert.prototype.confirmCheck = function(dialogTitle, text, checkMsg, checkValue) {
   fxdriver.modals.signalOpenModal(this.parentWindow_, text);
-  return this.delegate_.confirmCheck(dialogTitle, text, checkMsg,  checkValue);
+  return this.delegate_.confirmCheck(dialogTitle, text, checkMsg, checkValue);
 };
 
 ObservingAlert.prototype.confirmEx = function(dialogTitle, text,
@@ -100,17 +100,17 @@
 
 // Spoof implementation
 function DrivenPromptService() {
-  fxdriver.logging.info("Spoofing prompt service");
+  fxdriver.logging.info('Spoofing prompt service');
 
   // @mozilla.org/prompter;1
   var prompters = [
-    "{1c978d25-b37f-43a8-a2d6-0c7a239ead87}" // nsPrompter.js: Firefox 4 late betas onwards
+    '{1c978d25-b37f-43a8-a2d6-0c7a239ead87}' // nsPrompter.js: Firefox 4 late betas onwards
   ];
 
   // @mozilla.org/embedcomp/prompt-service;
   var promptServices = [
-    "{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}", // nsPrompter.js: Firefox 4 betas
-    "{A2112D6A-0E28-421f-B46A-25C0B308CBD0}"  // nsPromptService.h: Firefox 3.x
+    '{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}', // nsPrompter.js: Firefox 4 betas
+    '{A2112D6A-0E28-421f-B46A-25C0B308CBD0}'  // nsPromptService.h: Firefox 3.x
   ];
 
   var findImplementation = function(interfaceName, cids) {
@@ -123,7 +123,7 @@
 
       try {
         var toReturn = service.QueryInterface(interfaceName);
-        fxdriver.logging.info("Found implementation at: " + cids[i]);
+        fxdriver.logging.info('Found implementation at: ' + cids[i]);
         return toReturn;
       } catch (ignored) {}
     }
@@ -136,11 +136,11 @@
   var originalPrompter_ = findImplementation(CI.nsIPromptFactory, prompters);
 
   if (!originalPromptService_) {
-    fxdriver.logging.info("Unable to locate original prompt service");
+    fxdriver.logging.info('Unable to locate original prompt service');
   }
 
   if (!originalPrompter_) {
-    fxdriver.logging.info("Unable to locate original prompter");
+    fxdriver.logging.info('Unable to locate original prompter');
   }
 
   this.delegate_ = originalPrompter_ ? originalPrompter_ : originalPromptService_;
@@ -151,7 +151,7 @@
   this.QueryInterface = fxdriver.moz.queryInterface(this,
     [CI.nsIPromptFactory, CI.nsIPromptService, CI.nsIPromptService2]);
 
-  fxdriver.logging.info("Finished initializing spoofed prompt service");
+  fxdriver.logging.info('Finished initializing spoofed prompt service');
 }
 
 // Constants from nsIPromtService.idl
@@ -269,15 +269,15 @@
   fxdriver.modals.signalOpenModal(aParent, '');
 
   var service = this.delegate_.QueryInterface(CI.nsIPromptService2);
-  return service.asyncPromptAuth(aParent, aChannel, aCallback, aContext, level, authInfo, checkboxLabel,checkValue)
+  return service.asyncPromptAuth(aParent, aChannel, aCallback, aContext, level, authInfo, checkboxLabel, checkValue);
 };
 
 
 // nsIPromptFactory
-DrivenPromptService.prototype.getPrompt = function (domWin, iid) {
+DrivenPromptService.prototype.getPrompt = function(domWin, iid) {
   var factory = this.delegate_.QueryInterface(CI.nsIPromptFactory);
   var rawPrompt = factory.getPrompt(domWin, iid);
-  
+
   return new ObservingAlert(domWin, rawPrompt);
 };
 
@@ -287,8 +287,8 @@
 };
 
 
-/** @const */ var PROMPT_SERVICE_CONTRACT_ID = "@mozilla.org/embedcomp/prompt-service;1";
-/** @const */ var PROMPTER_CONTRACT_ID = "@mozilla.org/prompter;1";
+/** @const */ var PROMPT_SERVICE_CONTRACT_ID = '@mozilla.org/embedcomp/prompt-service;1';
+/** @const */ var PROMPTER_CONTRACT_ID = '@mozilla.org/prompter;1';
 
 // This is defined by us
 /** @const */ var DRIVEN_PROMPT_SERVICE_CLASS_ID = Components.ID('{e26dbdcd-d3ba-4ded-88c3-6cb07ee3e9e0}');
@@ -296,7 +296,7 @@
 var service = undefined;
 
 var PromptServiceSpoofFactory = {
-  createInstance: function (aOuter, aIID) {
+  createInstance: function(aOuter, aIID) {
     if (aOuter != null)
       throw Components.results.NS_ERROR_NO_AGGREGATION;
     if (service == undefined) {
@@ -317,13 +317,13 @@
   }
   aCompMgr = aCompMgr.QueryInterface(CI.nsIComponentRegistrar);
   aCompMgr.registerFactoryLocation(
-      DRIVEN_PROMPT_SERVICE_CLASS_ID, "Driven prompt service", PROMPT_SERVICE_CONTRACT_ID, aFileSpec, aLocation, aType);
+      DRIVEN_PROMPT_SERVICE_CLASS_ID, 'Driven prompt service', PROMPT_SERVICE_CONTRACT_ID, aFileSpec, aLocation, aType);
   aCompMgr.registerFactoryLocation(
-      DRIVEN_PROMPT_SERVICE_CLASS_ID, "Driven prompter service", PROMPTER_CONTRACT_ID, aFileSpec, aLocation, aType);
+      DRIVEN_PROMPT_SERVICE_CLASS_ID, 'Driven prompter service', PROMPTER_CONTRACT_ID, aFileSpec, aLocation, aType);
 };
 
 PromptServiceSpoofModule.prototype.unregisterSelf = function(aCompMgr, aLocation, aType) {
-  fxdriver.logging.info("Unregistering\n");
+  fxdriver.logging.info('Unregistering\n');
   aCompMgr.QueryInterface(CI.nsIComponentRegistrar);
   aCompMgr.unregisterFactoryLocation(DRIVEN_PROMPT_SERVICE_CLASS_ID, aLocation);
 };
diff --git a/javascript/firefox-driver/js/proxy.js b/javascript/firefox-driver/js/proxy.js
index b18a0ff..5b5b35b 100644
--- a/javascript/firefox-driver/js/proxy.js
+++ b/javascript/firefox-driver/js/proxy.js
@@ -133,7 +133,7 @@
 fxdriver.proxy.systemConfig_ = function(prefs, ignored) {
   fxdriver.logging.info('Using system proxy to connect to the network');
 
-  prefs.setIntPref('network.proxy.type', 
+  prefs.setIntPref('network.proxy.type',
     fxdriver.proxy.TYPES_['SYSTEM'].value);
 };
 
diff --git a/javascript/firefox-driver/js/sessionstore.js b/javascript/firefox-driver/js/sessionstore.js
index 48d418a..9d8e4f8 100644
--- a/javascript/firefox-driver/js/sessionstore.js
+++ b/javascript/firefox-driver/js/sessionstore.js
@@ -22,6 +22,7 @@
 goog.require('fxdriver.modals');
 goog.require('fxdriver.moz');
 goog.require('fxdriver.proxy');
+goog.require('goog.object');
 goog.require('wdSession');
 
 /**
@@ -115,9 +116,9 @@
 };
 
 /**
- * Extract the setting for a capability. 
+ * Extract the setting for a capability.
  *
- * If a capability is defined both among desired capabilities and among 
+ * If a capability is defined both among desired capabilities and among
  * required capabilities the required setting has priority.
  *
  * @private
@@ -150,7 +151,7 @@
 };
 
 /**
- * Read-write capabilities for FirefoxDriver corresponding to (boolean) 
+ * Read-write capabilities for FirefoxDriver corresponding to (boolean)
  * profile preferences. NB! the native events capability is not mapped to a
  * Firefox preferences.
  * @type {!Object.<string, string>}
@@ -218,9 +219,9 @@
       fxdriver.logging.info('Setting capability ' +
         key + ' (' + pref + ') to ' + value);
       if (key == 'nativeEvents') {
-        driver.enableNativeEvents = value; 
+        driver.enableNativeEvents = value;
       }
-    } 
+    }
   });
 };
 
@@ -319,7 +320,7 @@
 };
 
 
-/** @see nsIModule.unregisterSelf */ 
+/** @see nsIModule.unregisterSelf */
 wdSessionStoreServiceModule.prototype.unregisterSelf = function(aCompMgr, aLocation) {
   aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar).
       unregisterFactoryLocation(wdSessionStoreService.CLASS_ID, aLocation);
@@ -352,7 +353,7 @@
 };
 
 wdSessionStoreService.prototype.classID = wdSessionStoreService.CLASS_ID;
-fxdriver.moz.load("resource://gre/modules/XPCOMUtils.jsm");
+fxdriver.moz.load('resource://gre/modules/XPCOMUtils.jsm');
 if (XPCOMUtils.generateNSGetFactory) {
   /** @const */ NSGetFactory = XPCOMUtils.generateNSGetFactory([wdSessionStoreService]);
 }
diff --git a/javascript/firefox-driver/js/syntheticMouse.js b/javascript/firefox-driver/js/syntheticMouse.js
index fb5b7b8..2d183c8 100644
--- a/javascript/firefox-driver/js/syntheticMouse.js
+++ b/javascript/firefox-driver/js/syntheticMouse.js
@@ -20,16 +20,18 @@
 goog.require('Utils');
 goog.require('bot.ErrorCode');
 goog.require('bot.Mouse');
-goog.require('bot.events.EventType');
 goog.require('bot.action');
 goog.require('bot.dom');
 goog.require('bot.events');
+goog.require('bot.events.EventType');
 goog.require('bot.window');
+goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
 goog.require('fxdriver.utils');
-goog.require('fxdriver.logging');
-goog.require('goog.events.EventType');
+goog.require('goog.dom');
+goog.require('goog.dom.TagName');
 goog.require('goog.math.Coordinate');
+goog.require('goog.style');
 
 
 SyntheticMouse = function() {
@@ -48,7 +50,7 @@
   // track of the viewport scroll offset in this variable, when we mouseDown,
   // until we mouseUp, so that we can account for any scrolling which may have
   // happened, when we fire events.
-  this.viewPortOffset = new goog.math.Coordinate(0,0);
+  this.viewPortOffset = new goog.math.Coordinate(0, 0);
 };
 
 
@@ -69,7 +71,7 @@
 SyntheticMouse.prototype.isElementShown = function(element) {
   if (!bot.dom.isShown(element, /*ignoreOpacity=*/true)) {
     return SyntheticMouse.newResponse(bot.ErrorCode.ELEMENT_NOT_VISIBLE,
-        'Element is not currently visible and so may not be interacted with')
+        'Element is not currently visible and so may not be interacted with');
   }
 };
 
@@ -126,7 +128,7 @@
 
   mouse.move(element, coords);
 
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 
@@ -144,18 +146,18 @@
   // Check to see if this is an option element. If it is, and the parent isn't a multiple
   // select, then click on the select first.
   var tagName = element.tagName.toLowerCase();
-  if ("option" == tagName) {
+  if ('option' == tagName) {
     var parent = element;
-    while (parent.parentNode != null && parent.tagName.toLowerCase() != "select") {
+    while (parent.parentNode != null && parent.tagName.toLowerCase() != 'select') {
       parent = parent.parentNode;
     }
 
-    if (parent && parent.tagName.toLowerCase() == "select" && !parent.multiple) {
+    if (parent && parent.tagName.toLowerCase() == 'select' && !parent.multiple) {
       bot.action.click(parent);
     }
   }
 
-  fxdriver.logging.info("About to do a bot.action.click on " + element);
+  fxdriver.logging.info('About to do a bot.action.click on ' + element);
   var keyboardState = new bot.Device.ModifiersState();
   if (this.modifierKeys !== undefined) {
     keyboardState.setPressed(bot.Device.Modifier.SHIFT, this.modifierKeys.isShiftPressed());
@@ -167,7 +169,7 @@
   var mouseWithKeyboardState = new bot.Mouse(null, keyboardState);
 
   bot.action.click(element, undefined /* coords */, mouseWithKeyboardState);
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 SyntheticMouse.prototype.contextClick = function(target) {
@@ -181,10 +183,10 @@
     return error;
   }
 
-  fxdriver.logging.info("About to do a bot.action.rightClick on " + element);
+  fxdriver.logging.info('About to do a bot.action.rightClick on ' + element);
   bot.action.rightClick(element);
 
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 SyntheticMouse.prototype.doubleClick = function(target) {
@@ -195,10 +197,10 @@
     return error;
   }
 
-  fxdriver.logging.info("About to do a bot.action.doubleClick on " + element);
+  fxdriver.logging.info('About to do a bot.action.doubleClick on ' + element);
   bot.action.doubleClick(element);
 
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 
@@ -224,7 +226,7 @@
   this.addEventModifierKeys(botCoords);
   bot.events.fire(element, bot.events.EventType.MOUSEDOWN, botCoords);
 
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 
@@ -250,11 +252,11 @@
   this.viewPortOffset.x = 0;
   this.viewPortOffset.y = 0;
 
-  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, "ok");
+  return SyntheticMouse.newResponse(bot.ErrorCode.SUCCESS, 'ok');
 };
 
 
-SyntheticMouse.prototype.addEventModifierKeys = function (botCoords) {
+SyntheticMouse.prototype.addEventModifierKeys = function(botCoords) {
   if (this.modifierKeys !== undefined) {
     botCoords.altKey = this.modifierKeys.isAltPressed();
     botCoords.ctrlKey = this.modifierKeys.isControlPressed();
@@ -265,7 +267,7 @@
 
 
 // And finally, registering
-SyntheticMouse.prototype.classDescription = "Pure JS implementation of a mouse";
+SyntheticMouse.prototype.classDescription = 'Pure JS implementation of a mouse';
 SyntheticMouse.prototype.contractID = '@googlecode.com/webdriver/syntheticmouse;1';
 SyntheticMouse.prototype.classID = Components.ID('{E8F9FEFE-C513-4097-98BE-BE00A41D3645}');
 
diff --git a/javascript/firefox-driver/js/utils.js b/javascript/firefox-driver/js/utils.js
index 71e89b3..b5a52d6 100644
--- a/javascript/firefox-driver/js/utils.js
+++ b/javascript/firefox-driver/js/utils.js
@@ -26,9 +26,9 @@
 goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
 goog.require('fxdriver.utils');
-goog.require('goog.dom.TagName');
-goog.require('goog.style');
+goog.require('goog.dom');
 goog.require('goog.string');
+goog.require('goog.style');
 
 
 /**
@@ -85,7 +85,7 @@
    * @type {!boolean}
    */
   this.isWebDriverError = true;
-}
+};
 
 function notifyOfCloseWindow(windowId) {
   windowId = windowId || 0;
@@ -110,7 +110,7 @@
   var clazz = Components.classes[className];
 
   if (!clazz) {
-    fxdriver.logging.warning("Unable to find class: " + className);
+    fxdriver.logging.warning('Unable to find class: ' + className);
     return undefined;
   }
   var iface = Components.interfaces[interfaceName];
@@ -118,7 +118,7 @@
   try {
     return clazz.createInstance(iface);
   } catch (e) {
-    fxdriver.logging.warning("Cannot create: " + className + " from " + interfaceName);
+    fxdriver.logging.warning('Cannot create: ' + className + ' from ' + interfaceName);
     fxdriver.logging.warning(e);
     throw e;
   }
@@ -127,7 +127,7 @@
 
 Utils.getServer = function() {
   var handle =
-      Utils.newInstance("@googlecode.com/webdriver/fxdriver;1", "nsISupports");
+      Utils.newInstance('@googlecode.com/webdriver/fxdriver;1', 'nsISupports');
   return handle.wrappedJSObject;
 };
 
@@ -136,7 +136,7 @@
   var window = goog.dom.getWindow(doc);
 
   var element;
-  if (doc["activeElement"]) {
+  if (doc['activeElement']) {
     element = doc.activeElement;
   } else {
     var topWindow = window.top;
@@ -197,29 +197,29 @@
   try {
     var obj = Components.classes[componentId].createInstance();
     return obj.QueryInterface(componentInterface);
-  } catch(e) {
-    fxdriver.logging.warning("Unable to find native component: " + componentId);
+  } catch (e) {
+    fxdriver.logging.warning('Unable to find native component: ' + componentId);
     fxdriver.logging.warning(e);
     // Unable to retrieve native events. No biggie, because we fall back to
     // synthesis later
     return undefined;
   }
-}
+};
 
 Utils.getNativeEvents = function() {
-  return Utils.getNativeComponent("@openqa.org/nativeevents;1", Components.interfaces.nsINativeEvents);
+  return Utils.getNativeComponent('@openqa.org/nativeevents;1', Components.interfaces.nsINativeEvents);
 };
 
 Utils.getNativeMouse = function() {
-  return Utils.getNativeComponent("@openqa.org/nativemouse;1", Components.interfaces.nsINativeMouse);
+  return Utils.getNativeComponent('@openqa.org/nativemouse;1', Components.interfaces.nsINativeMouse);
 };
 
 Utils.getNativeKeyboard = function() {
-  return Utils.getNativeComponent("@openqa.org/nativekeyboard;1", Components.interfaces.nsINativeKeyboard);
+  return Utils.getNativeComponent('@openqa.org/nativekeyboard;1', Components.interfaces.nsINativeKeyboard);
 };
 
 Utils.getNativeIME = function() {
-  return Utils.getNativeComponent("@openqa.org/nativeime;1", Components.interfaces.nsINativeIME);
+  return Utils.getNativeComponent('@openqa.org/nativeime;1', Components.interfaces.nsINativeIME);
 };
 
 Utils.getNodeForNativeEvents = function(element) {
@@ -227,12 +227,12 @@
     // This stuff changes between releases.
     // Do as much up-front work in JS as possible
     var retrieval = Utils.newInstance(
-        "@mozilla.org/accessibleRetrieval;1", "nsIAccessibleRetrieval");
+        '@mozilla.org/accessibleRetrieval;1', 'nsIAccessibleRetrieval');
     var accessible = retrieval.getAccessibleFor(element.ownerDocument);
     var accessibleDoc =
         accessible.QueryInterface(Components.interfaces.nsIAccessibleDocument);
     return accessibleDoc.QueryInterface(Components.interfaces.nsISupports);
-  } catch(e) {
+  } catch (e) {
     // Unable to retrieve the accessible doc
     return undefined;
   }
@@ -240,10 +240,10 @@
 
 Utils.useNativeEvents = function() {
   var prefs =
-    fxdriver.moz.getService("@mozilla.org/preferences-service;1", "nsIPrefBranch");
+    fxdriver.moz.getService('@mozilla.org/preferences-service;1', 'nsIPrefBranch');
   var enableNativeEvents =
-    prefs.prefHasUserValue("webdriver_enable_native_events") ?
-    prefs.getBoolPref("webdriver_enable_native_events") : false;
+    prefs.prefHasUserValue('webdriver_enable_native_events') ?
+    prefs.getBoolPref('webdriver_enable_native_events') : false;
 
   return !!(enableNativeEvents && Utils.getNativeEvents());
 };
@@ -259,7 +259,7 @@
 
   var obj = Utils.getNativeKeyboard();
   var node = Utils.getNodeForNativeEvents(element);
-  var thmgr_cls = Components.classes["@mozilla.org/thread-manager;1"];
+  var thmgr_cls = Components.classes['@mozilla.org/thread-manager;1'];
   var isUsingNativeEvents = opt_useNativeEvents && obj && node && thmgr_cls;
 
   if (isUsingNativeEvents) {
@@ -273,7 +273,7 @@
     return;
   }
 
-  fxdriver.logging.info("Doing sendKeys in a non-native way...");
+  fxdriver.logging.info('Doing sendKeys in a non-native way...');
   var controlKey = false;
   var shiftKey = false;
   var altKey = false;
@@ -297,25 +297,25 @@
     if (c == '\uE000') {
       if (controlKey) {
         var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_CONTROL;
-        Utils.keyEvent(doc, element, "keyup", kCode, 0,
+        Utils.keyEvent(doc, element, 'keyup', kCode, 0,
             controlKey = false, shiftKey, altKey, metaKey, false);
       }
 
       if (shiftKey) {
         var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_SHIFT;
-        Utils.keyEvent(doc, element, "keyup", kCode, 0,
+        Utils.keyEvent(doc, element, 'keyup', kCode, 0,
             controlKey, shiftKey = false, altKey, metaKey, false);
       }
 
       if (altKey) {
         var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_ALT;
-        Utils.keyEvent(doc, element, "keyup", kCode, 0,
+        Utils.keyEvent(doc, element, 'keyup', kCode, 0,
             controlKey, shiftKey, altKey = false, metaKey, false);
       }
 
       if (metaKey) {
         var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_META;
-        Utils.keyEvent(doc, element, "keyup", kCode, 0,
+        Utils.keyEvent(doc, element, 'keyup', kCode, 0,
             controlKey, shiftKey, altKey, metaKey = false, false);
       }
 
@@ -324,7 +324,7 @@
 
     // otherwise decode keyCode, charCode, modifiers ...
 
-    var modifierEvent = "";
+    var modifierEvent = '';
     var charCode = 0;
     var keyCode = 0;
 
@@ -345,19 +345,19 @@
     } else if (c == '\uE008') {
       keyCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_SHIFT;
       shiftKey = !shiftKey;
-      modifierEvent = shiftKey ? "keydown" : "keyup";
+      modifierEvent = shiftKey ? 'keydown' : 'keyup';
     } else if (c == '\uE009') {
       keyCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_CONTROL;
       controlKey = !controlKey;
-      modifierEvent = controlKey ? "keydown" : "keyup";
+      modifierEvent = controlKey ? 'keydown' : 'keyup';
     } else if (c == '\uE00A') {
       keyCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_ALT;
       altKey = !altKey;
-      modifierEvent = altKey ? "keydown" : "keyup";
+      modifierEvent = altKey ? 'keydown' : 'keyup';
     } else if (c == '\uE03D') {
       keyCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_META;
       metaKey = !metaKey;
-      modifierEvent = metaKey ? "keydown" : "keyup";
+      modifierEvent = metaKey ? 'keydown' : 'keyup';
     } else if (c == '\uE00B') {
       keyCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_PAUSE;
     } else if (c == '\uE00C') {
@@ -509,7 +509,7 @@
 
     if (needsShift && !shiftKey) {
       var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_SHIFT;
-      Utils.keyEvent(doc, element, "keydown", kCode, 0,
+      Utils.keyEvent(doc, element, 'keydown', kCode, 0,
           controlKey, true, altKey, metaKey, false);
       Utils.shiftCount += 1;
     }
@@ -540,20 +540,20 @@
     }
 
     var accepted =
-        Utils.keyEvent(doc, element, "keydown", keyCode, 0,
+        Utils.keyEvent(doc, element, 'keydown', keyCode, 0,
             controlKey, needsShift || shiftKey, altKey, metaKey, false);
 
-    Utils.keyEvent(doc, element, "keypress", pressCode, charCode,
+    Utils.keyEvent(doc, element, 'keypress', pressCode, charCode,
         controlKey, needsShift || shiftKey, altKey, metaKey, !accepted);
 
-    Utils.keyEvent(doc, element, "keyup", keyCode, 0,
+    Utils.keyEvent(doc, element, 'keyup', keyCode, 0,
         controlKey, needsShift || shiftKey, altKey, metaKey, false);
 
     // shift up if needed
 
     if (needsShift && !shiftKey) {
       var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_SHIFT;
-      Utils.keyEvent(doc, element, "keyup", kCode, 0,
+      Utils.keyEvent(doc, element, 'keyup', kCode, 0,
           controlKey, false, altKey, metaKey, false);
     }
   }
@@ -562,25 +562,25 @@
 
   if (controlKey && releaseModifiers) {
     var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_CONTROL;
-    Utils.keyEvent(doc, element, "keyup", kCode, 0,
+    Utils.keyEvent(doc, element, 'keyup', kCode, 0,
         controlKey = false, shiftKey, altKey, metaKey, false);
   }
 
   if (shiftKey && releaseModifiers) {
     var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_SHIFT;
-    Utils.keyEvent(doc, element, "keyup", kCode, 0,
+    Utils.keyEvent(doc, element, 'keyup', kCode, 0,
         controlKey, shiftKey = false, altKey, metaKey, false);
   }
 
   if (altKey && releaseModifiers) {
     var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_ALT;
-    Utils.keyEvent(doc, element, "keyup", kCode, 0,
+    Utils.keyEvent(doc, element, 'keyup', kCode, 0,
         controlKey, shiftKey, altKey = false, metaKey, false);
   }
 
   if (metaKey && releaseModifiers) {
     var kCode = Components.interfaces.nsIDOMKeyEvent.DOM_VK_META;
-    Utils.keyEvent(doc, element, "keyup", kCode, 0,
+    Utils.keyEvent(doc, element, 'keyup', kCode, 0,
         controlKey, shiftKey, altKey, metaKey = false, false);
   }
 
@@ -599,7 +599,7 @@
   var preventDefault = shouldPreventDefault == undefined ? false
       : shouldPreventDefault;
 
-  var keyboardEvent = doc.createEvent("KeyEvents");
+  var keyboardEvent = doc.createEvent('KeyEvents');
   var currentView = doc.defaultView;
 
   keyboardEvent.initKeyEvent(
@@ -631,7 +631,7 @@
 
 Utils.fireHtmlEvent = function(element, eventName) {
   var doc = element.ownerDocument;
-  var e = doc.createEvent("HTMLEvents");
+  var e = doc.createEvent('HTMLEvents');
   e.initEvent(eventName, true, true);
   return element.dispatchEvent(e);
 };
@@ -643,7 +643,7 @@
 
 
 Utils.triggerMouseEvent = function(element, eventType, clientX, clientY) {
-  var event = element.ownerDocument.createEvent("MouseEvents");
+  var event = element.ownerDocument.createEvent('MouseEvents');
   var view = element.ownerDocument.defaultView;
 
   clientX = clientX || 0;
@@ -660,7 +660,7 @@
   var y = element.offsetTop;
   var elementParent = element.offsetParent;
   while (elementParent != null) {
-    if (elementParent.tagName == "TABLE") {
+    if (elementParent.tagName == 'TABLE') {
       var parentBorder = parseInt(elementParent.border);
       if (isNaN(parentBorder)) {
         var parentFrame = elementParent.getAttribute('frame');
@@ -687,7 +687,7 @@
 
 Utils.getLocationViaAccessibilityInterface = function(element) {
   var retrieval = Utils.newInstance(
-    "@mozilla.org/accessibleRetrieval;1", "nsIAccessibleRetrieval");
+    '@mozilla.org/accessibleRetrieval;1', 'nsIAccessibleRetrieval');
   var accessible = retrieval.getAccessibleFor(element);
 
   if (! accessible) {
@@ -698,8 +698,8 @@
   accessible.getBounds(x, y, width, height);
 
   return {
-    x : x.value,
-    y : y.value,
+    x: x.value,
+    y: y.value,
     width: width.value,
     height: height.value
   };
@@ -727,8 +727,8 @@
     // Firefox 3.5
     if (clientRect['width']) {
       return {
-        x : clientRect.left,
-        y : clientRect.top,
+        x: clientRect.left,
+        y: clientRect.top,
         width: clientRect.width,
         height: clientRect.height
       };
@@ -739,22 +739,22 @@
       var retWidth = clientRect.right - clientRect.left;
       var retHeight = clientRect.bottom - clientRect.top;
       return {
-        x : clientRect.left,
-        y : clientRect.top,
+        x: clientRect.left,
+        y: clientRect.top,
         width: retWidth,
         height: retHeight
-      }
+      };
     }
 
     // Firefox 3.0, but lacking client rect
-    fxdriver.logging.info("Falling back to firefox3 mechanism");
+    fxdriver.logging.info('Falling back to firefox3 mechanism');
     var accessibleLocation = Utils.getLocationViaAccessibilityInterface(element);
     accessibleLocation.x = clientRect.left;
     accessibleLocation.y = clientRect.top;
     return accessibleLocation;
-  } catch(e) {
+  } catch (e) {
     // Element doesn't have an accessibility node
-    fxdriver.logging.warning("Falling back to using closure to find the location of the element");
+    fxdriver.logging.warning('Falling back to using closure to find the location of the element');
     fxdriver.logging.warning(e);
 
     var position = goog.style.getClientPosition(element);
@@ -810,8 +810,8 @@
     var rect = inBrowser.getBoundingClientRect();
     browserSpecificYOffset += rect.top;
     browserSpecificXOffset += rect.left;
-    fxdriver.logging.info("Browser-specific offset (X,Y): " + browserSpecificXOffset
-        + ", " + browserSpecificYOffset);
+    fxdriver.logging.info('Browser-specific offset (X,Y): ' + browserSpecificXOffset
+        + ', ' + browserSpecificYOffset);
   }
 
   return {x: browserSpecificXOffset, y: browserSpecificYOffset};
@@ -937,19 +937,19 @@
       return result;
   }
 };
-    
+
 
 Utils.loadUrl = function(url) {
-  fxdriver.logging.info("Loading: " + url);
-  var ioService = fxdriver.moz.getService("@mozilla.org/network/io-service;1", "nsIIOService");
+  fxdriver.logging.info('Loading: ' + url);
+  var ioService = fxdriver.moz.getService('@mozilla.org/network/io-service;1', 'nsIIOService');
   var channel = ioService.newChannel(url, null, null);
   var channelStream = channel.open();
 
-  var scriptableStream = Components.classes["@mozilla.org/scriptableinputstream;1"]
+  var scriptableStream = Components.classes['@mozilla.org/scriptableinputstream;1']
                                  .createInstance(Components.interfaces.nsIScriptableInputStream);
   scriptableStream.init(channelStream);
 
-  var converter = Utils.newInstance("@mozilla.org/intl/scriptableunicodeconverter",
+  var converter = Utils.newInstance('@mozilla.org/intl/scriptableunicodeconverter',
                       'nsIScriptableUnicodeConverter');
   converter.charset = 'UTF-8';
 
@@ -963,11 +963,11 @@
   scriptableStream.close();
   channelStream.close();
 
-  fxdriver.logging.info("Done reading: " + url);
+  fxdriver.logging.info('Done reading: ' + url);
   return text;
 };
 
-Utils.installWindowCloseListener = function (respond) {
+Utils.installWindowCloseListener = function(respond) {
   var browser = respond.session.getBrowser();
 
   // Override the "respond.send" function to remove the observer, otherwise
@@ -990,7 +990,7 @@
 
 
       if (target == source) {
-        fxdriver.logging.info("Window was closed.");
+        fxdriver.logging.info('Window was closed.');
         respond.send();
       }
     }
@@ -1036,7 +1036,7 @@
     var docLoaderService = browser.webProgress;
     if (!docLoaderService.isLoadingDocument) {
       WebLoadingListener.removeListener(browser, clickListener);
-      fxdriver.logging.info("Not loading document anymore.");
+      fxdriver.logging.info('Not loading document anymore.');
       respond.send();
     }
   };
@@ -1045,7 +1045,7 @@
   if (contentWindow.closed) {
     // Nulls out the session; client will have to switch to another
     // window on their own.
-    fxdriver.logging.info("Content window closed.");
+    fxdriver.logging.info('Content window closed.');
     respond.send();
     return;
   }
@@ -1053,7 +1053,7 @@
 };
 
 Utils.waitForNativeEventsProcessing = function(element, nativeEvents, pageUnloadedData, jsTimer) {
-  var thmgr_cls = Components.classes["@mozilla.org/thread-manager;1"];
+  var thmgr_cls = Components.classes['@mozilla.org/thread-manager;1'];
   var node = Utils.getNodeForNativeEvents(element);
 
   var hasEvents = {};
@@ -1070,7 +1070,7 @@
     var doneNativeEventWait = false;
 
     var callback = function() {
-      fxdriver.logging.info("Done native event wait.");
+      fxdriver.logging.info('Done native event wait.');
       doneNativeEventWait = true;
     };
 
@@ -1078,7 +1078,7 @@
 
     nativeEvents.hasUnhandledEvents(node, hasEvents);
 
-    fxdriver.logging.info("Pending native events: " + hasEvents.value);
+    fxdriver.logging.info('Pending native events: ' + hasEvents.value);
     var numEventsProcessed = 0;
     // Do it as long as the timeout function has not been called and the
     // page has not been unloaded. If the page has been unloaded, there is no
@@ -1089,15 +1089,15 @@
       thread.processNextEvent(true);
       numEventsProcessed += 1;
     }
-    fxdriver.logging.info("Extra events processed: " + numEventsProcessed +
-                 " Page Unloaded: " + pageUnloadedData.wasUnloaded);
+    fxdriver.logging.info('Extra events processed: ' + numEventsProcessed +
+                 ' Page Unloaded: ' + pageUnloadedData.wasUnloaded);
 
   } while ((hasEvents.value == true) && (!pageUnloadedData.wasUnloaded));
-  fxdriver.logging.info("Done main loop.");
+  fxdriver.logging.info('Done main loop.');
 
   if (pageUnloadedData.wasUnloaded) {
-      fxdriver.logging.info("Page has been reloaded while waiting for native events to "
-          + "be processed. Remaining events? " + hasEvents.value);
+      fxdriver.logging.info('Page has been reloaded while waiting for native events to '
+          + 'be processed. Remaining events? ' + hasEvents.value);
   } else {
     Utils.removePageUnloadEventListener(element, pageUnloadedData);
   }
@@ -1123,7 +1123,7 @@
     numExtraEventsProcessed += 1;
   }
 
-  fxdriver.logging.info("Done extra event loop, " + numExtraEventsProcessed);
+  fxdriver.logging.info('Done extra event loop, ' + numExtraEventsProcessed);
 };
 
 Utils.getPageUnloadedIndicator = function(element) {
@@ -1139,12 +1139,12 @@
   var unloadFunction = function() { toReturn.wasUnloaded = true };
   toReturn.callback = unloadFunction;
 
-  element.ownerDocument.body.addEventListener("unload",
+  element.ownerDocument.body.addEventListener('unload',
       unloadFunction, false);
 
   // This is a Firefox specific event - See:
   // https://developer.mozilla.org/En/Using_Firefox_1.5_caching
-  element.ownerDocument.defaultView.addEventListener("pagehide",
+  element.ownerDocument.defaultView.addEventListener('pagehide',
       unloadFunction, false);
 
   return toReturn;
@@ -1155,11 +1155,11 @@
     // Remove event listeners...
     if (element.ownerDocument) {
       if (element.ownerDocument.body) {
-        element.ownerDocument.body.removeEventListener("unload",
+        element.ownerDocument.body.removeEventListener('unload',
             pageUnloadData.callback, false);
       }
       if (element.ownerDocument.defaultView) {
-        element.ownerDocument.defaultView.removeEventListener("pagehide",
+        element.ownerDocument.defaultView.removeEventListener('pagehide',
             pageUnloadData.callback, false);
       }
     }
diff --git a/javascript/firefox-driver/js/wdsession.js b/javascript/firefox-driver/js/wdsession.js
index 3416f9d..848f0dd 100644
--- a/javascript/firefox-driver/js/wdsession.js
+++ b/javascript/firefox-driver/js/wdsession.js
@@ -233,7 +233,7 @@
     if (win == win.top) {
       self.window_ = null;
     }
-  }
+  };
 
   // Listen in capture mode to force us to be called (can't stop event
   // propagation in capture mode)
@@ -354,7 +354,7 @@
 wdSession.prototype.setMouseViewportOffset = function(x, y) {
   this.mousePosition_.viewPortXOffset = x;
   this.mousePosition_.viewPortYOffset = y;
-}
+};
 
 /**
  * Close the browser after a given time delay.
@@ -462,8 +462,8 @@
   return new wdSessionModule();
 };
 
-wdSession.prototype.classID = wdSession.CLASS_ID
-fxdriver.moz.load("resource://gre/modules/XPCOMUtils.jsm");
+wdSession.prototype.classID = wdSession.CLASS_ID;
+fxdriver.moz.load('resource://gre/modules/XPCOMUtils.jsm');
 if (XPCOMUtils.generateNSGetFactory) {
   /** @const */ NSGetFactory = XPCOMUtils.generateNSGetFactory([wdSession]);
 }
diff --git a/javascript/firefox-driver/js/webLoadingListener.js b/javascript/firefox-driver/js/webLoadingListener.js
index 614d8db..6712b5d 100644
--- a/javascript/firefox-driver/js/webLoadingListener.js
+++ b/javascript/firefox-driver/js/webLoadingListener.js
@@ -36,9 +36,9 @@
   }
 
   var ioService =
-      fxdriver.moz.getService("@mozilla.org/network/io-service;1", "nsIIOService");
-  var currentUri = ioService.newURI(current, "", null);
-  var futureUri = ioService.newURI(future, "", currentUri);
+      fxdriver.moz.getService('@mozilla.org/network/io-service;1', 'nsIIOService');
+  var currentUri = ioService.newURI(current, '', null);
+  var futureUri = ioService.newURI(future, '', currentUri);
 
   var loadEventExpected = true;
   if (futureUri.scheme == 'javascript') {
@@ -52,7 +52,7 @@
     // Looks like we're at the same url with a ref
     // Being clever and checking the ref was causing me headaches.
     // Brute force for now
-    loadEventExpected = futureUri.path.indexOf("#") == -1;
+    loadEventExpected = futureUri.path.indexOf('#') == -1;
   }
 
   return loadEventExpected;
@@ -158,7 +158,7 @@
 };
 
 
-var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces["nsIPrefBranch"]);
+var prefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces['nsIPrefBranch']);
 
 function buildHandler(browser, toCall, opt_window) {
   if (prefs.prefHasUserValue('webdriver.load.strategy')) {
diff --git a/javascript/firefox-driver/js/webdriverserver.js b/javascript/firefox-driver/js/webdriverserver.js
index 85919a3..2621794 100644
--- a/javascript/firefox-driver/js/webdriverserver.js
+++ b/javascript/firefox-driver/js/webdriverserver.js
@@ -31,25 +31,25 @@
 WebDriverServer = function() {
   this.wrappedJSObject = this;
   this.serverSocket =
-  Components.classes["@mozilla.org/network/server-socket;1"].
+  Components.classes['@mozilla.org/network/server-socket;1'].
       createInstance(Components.interfaces.nsIServerSocket);
-  this.generator = fxdriver.moz.getService("@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+  this.generator = fxdriver.moz.getService('@mozilla.org/uuid-generator;1', 'nsIUUIDGenerator');
   this.enableNativeEvents = null;
 
   // Force our cert override service to be loaded - otherwise, it will not be
   // loaded and cause a "too deep recursion" error.
-  var overrideService = Components.classes["@mozilla.org/security/certoverride;1"]
+  var overrideService = Components.classes['@mozilla.org/security/certoverride;1']
       .getService(Components.interfaces.nsICertOverrideService);
 
   var dispatcher_ = new Dispatcher();
 
   try {
-    this.server_ = Utils.newInstance("@mozilla.org/server/jshttp;1", "nsIHttpServer");
+    this.server_ = Utils.newInstance('@mozilla.org/server/jshttp;1', 'nsIHttpServer');
   } catch (e) {
     fxdriver.logging.warning(e);
   }
 
-  this.server_.registerGlobHandler(".*/hub/.*", { handle: function(request, response) {
+  this.server_.registerGlobHandler('.*/hub/.*', { handle: function(request, response) {
     response.processAsync();
     dispatcher_.dispatch(new Request(request), new Response(response));
   }});
@@ -74,10 +74,10 @@
 WebDriverServer.prototype.startListening = function(port) {
   if (!port) {
     var prefs =
-        fxdriver.moz.getService("@mozilla.org/preferences-service;1", "nsIPrefBranch");
+        fxdriver.moz.getService('@mozilla.org/preferences-service;1', 'nsIPrefBranch');
 
-    port = prefs.prefHasUserValue("webdriver_firefox_port") ?
-           prefs.getIntPref("webdriver_firefox_port") : 7055;
+    port = prefs.prefHasUserValue('webdriver_firefox_port') ?
+           prefs.getIntPref('webdriver_firefox_port') : 7055;
   }
 
   if (!this.isListening) {
diff --git a/javascript/firefox-driver/js/wrappedElement.js b/javascript/firefox-driver/js/wrappedElement.js
index f0f892f..137d232 100644
--- a/javascript/firefox-driver/js/wrappedElement.js
+++ b/javascript/firefox-driver/js/wrappedElement.js
@@ -24,13 +24,14 @@
 goog.require('bot.ErrorCode');
 goog.require('bot.action');
 goog.require('bot.dom');
-goog.require('fxdriver.logging');
 goog.require('fxdriver.io');
+goog.require('fxdriver.logging');
 goog.require('fxdriver.moz');
 goog.require('fxdriver.preconditions');
 goog.require('goog.dom');
 goog.require('goog.dom.TagName');
 goog.require('goog.dom.selection');
+goog.require('goog.math.Coordinate');
 goog.require('webdriver.atoms.element');
 
 
@@ -60,27 +61,27 @@
   var unwrapped = fxdriver.moz.unwrapFor4(element);
   var nativeMouse = Utils.getNativeMouse();
   var node = Utils.getNodeForNativeEvents(unwrapped);
-  var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
+  var appInfo = Components.classes['@mozilla.org/xre/app-info;1'].
       getService(Components.interfaces.nsIXULAppInfo);
   var versionChecker = Components.
-      classes["@mozilla.org/xpcom/version-comparator;1"].
+      classes['@mozilla.org/xpcom/version-comparator;1'].
       getService(Components.interfaces.nsIVersionComparator);
 
   // I'm having trouble getting clicks to work on Firefox 2 on Windows. Always
   // fall back for that
   var useNativeClick =
-      versionChecker.compare(appInfo.platformVersion, "1.9") >= 0;
-  var thmgr_cls = Components.classes["@mozilla.org/thread-manager;1"];
+      versionChecker.compare(appInfo.platformVersion, '1.9') >= 0;
+  var thmgr_cls = Components.classes['@mozilla.org/thread-manager;1'];
 
   // For now, we need to bypass native events for option elements
-  var isOption = "option" == unwrapped.tagName.toLowerCase();
+  var isOption = 'option' == unwrapped.tagName.toLowerCase();
 
-  var location = Utils.getLocation(unwrapped, unwrapped.tagName == "A");
+  var location = Utils.getLocation(unwrapped, unwrapped.tagName == 'A');
   var elementHalfWidth = (location.width ? location.width / 2 : 0);
   var elementHalfHeight = (location.height ? location.height / 2 : 0);
 
   if (!isOption && this.enableNativeEvents && nativeMouse && node && useNativeClick && thmgr_cls) {
-    fxdriver.logging.info("Using native events for click");
+    fxdriver.logging.info('Using native events for click');
 
     var inViewAfterScroll = bot.action.scrollIntoView(
         unwrapped,
@@ -93,7 +94,7 @@
         return;
     }
 
-    location = Utils.getLocationRelativeToWindowHandle(unwrapped, unwrapped.tagName == "A");
+    location = Utils.getLocationRelativeToWindowHandle(unwrapped, unwrapped.tagName == 'A');
     var x = location.x + elementHalfWidth;
     var y = location.y + elementHalfHeight;
 
@@ -124,9 +125,9 @@
       // the error returned from the native call indicates it's not
       // implemented.
 
-      fxdriver.logging.info("Detected error when clicking: " + e.name);
+      fxdriver.logging.info('Detected error when clicking: ' + e.name);
 
-      if (e.name != "NS_ERROR_NOT_IMPLEMENTED") {
+      if (e.name != 'NS_ERROR_NOT_IMPLEMENTED') {
         throw new WebDriverError(bot.ErrorCode.INVALID_ELEMENT_STATE, e);
       }
 
@@ -134,7 +135,7 @@
     }
   }
 
-  fxdriver.logging.info("Falling back to synthesized click");
+  fxdriver.logging.info('Falling back to synthesized click');
 
   // TODO(simon): Delete the above and sink most of it into a "nativeMouse"
   Utils.installWindowCloseListener(respond);
@@ -153,7 +154,7 @@
   respond.value = res.message;
 };
 WebElement.clickElement.preconditions =
-    [ fxdriver.preconditions.visible ];
+    [fxdriver.preconditions.visible];
 
 
 WebElement.getElementText = function(respond, parameters) {
@@ -183,32 +184,32 @@
   var newDocument = goog.dom.getOwnerDocument(currentlyActive);
 
   if (currentlyActive != element || currentDocument != new XPCNativeWrapper(newDocument)) {
-    fxdriver.logging.info("Need to switch focus");
+    fxdriver.logging.info('Need to switch focus');
     alreadyFocused = false;
     currentlyActive.blur();
     element.focus();
     element.ownerDocument.defaultView.focus();
   } else {
-    fxdriver.logging.info("No need to switch focus");
+    fxdriver.logging.info('No need to switch focus');
   }
 
   var use = element;
   var tagName = element.tagName.toLowerCase();
-  if (tagName == "body" && element.ownerDocument.defaultView.frameElement) {
+  if (tagName == 'body' && element.ownerDocument.defaultView.frameElement) {
     element.ownerDocument.defaultView.focus();
 
     // Turns out, this is what we should be using as the target
     // to send events to
-    use = element.ownerDocument.getElementsByTagName("html")[0];
+    use = element.ownerDocument.getElementsByTagName('html')[0];
   }
 
   // Handle the special case of the file input element here
 
   if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
-    var inputtype = element.getAttribute("type");
-    if (inputtype && inputtype.toLowerCase() == "file") {
+    var inputtype = element.getAttribute('type');
+    if (inputtype && inputtype.toLowerCase() == 'file') {
       element.value = parameters.value.join('');
-      Utils.fireHtmlEvent(element, "change");
+      Utils.fireHtmlEvent(element, 'change');
       respond.send();
       return;
     }
@@ -220,7 +221,7 @@
   this.jsTimer.setTimeout(function() {
     // Unless the element already had focus, set the cursor location to the end of the line
     // TODO(simon): This seems a little arbitrary.
-    if(!alreadyFocused && bot.dom.isEditable(element)) {
+    if (!alreadyFocused && bot.dom.isEditable(element)) {
         var length = element.value ? element.value.length : goog.dom.getTextContent(element).length;
         goog.dom.selection.setCursorPosition(element, length);
     }
@@ -232,7 +233,7 @@
   }, 0);
 };
 WebElement.sendKeysToElement.preconditions =
-    [ fxdriver.preconditions.visible, fxdriver.preconditions.enabled ];
+    [fxdriver.preconditions.visible, fxdriver.preconditions.enabled];
 
 
 WebElement.clearElement = function(respond, parameters) {
@@ -243,7 +244,7 @@
   respond.send();
 };
 WebElement.clearElement.preconditions =
-    [ fxdriver.preconditions.visible, fxdriver.preconditions.enabled, fxdriver.preconditions.writable ];
+    [fxdriver.preconditions.visible, fxdriver.preconditions.enabled, fxdriver.preconditions.writable];
 
 
 WebElement.getElementTagName = function(respond, parameters) {
@@ -259,7 +260,7 @@
   var element = Utils.getElementAt(parameters.id,
                                   respond.session.getDocument());
   var attributeName = parameters.name;
-  
+
   respond.value = webdriver.atoms.element.getAttribute(element, attributeName);
   respond.send();
 };
@@ -278,12 +279,12 @@
                                    respond.session.getDocument());
 
   if (element) {
-    while (element.parentNode != null && element.tagName.toLowerCase() != "form") {
+    while (element.parentNode != null && element.tagName.toLowerCase() != 'form') {
       element = element.parentNode;
     }
-    if (element.tagName && element.tagName.toLowerCase() == "form") {
+    if (element.tagName && element.tagName.toLowerCase() == 'form') {
       var current = respond.session.getWindow().location;
-      if (Utils.fireHtmlEvent(element, "submit") &&
+      if (Utils.fireHtmlEvent(element, 'submit') &&
           fxdriver.io.isLoadExpected(current, element.action)) {
         new WebLoadingListener(respond.session.getBrowser(), function(timedOut) {
           if (timedOut) {
@@ -318,16 +319,16 @@
     var option =
         element.QueryInterface(Components.interfaces.nsIDOMHTMLOptionElement);
     selected = option.selected;
-  } catch(e) {
+  } catch (e) {
   }
 
   try {
     var inputElement =
         element.QueryInterface(Components.interfaces.nsIDOMHTMLInputElement);
-    if (inputElement.type == "checkbox" || inputElement.type == "radio") {
+    if (inputElement.type == 'checkbox' || inputElement.type == 'radio') {
       selected = inputElement.checked;
     }
-  } catch(e) {
+  } catch (e) {
   }
 
  respond.value = selected;
@@ -397,8 +398,8 @@
   var elementLocation = Utils.getLocationOnceScrolledIntoView(element);
 
   respond.value = {
-    x : Math.round(elementLocation.x),
-    y : Math.round(elementLocation.y)
+    x: Math.round(elementLocation.x),
+    y: Math.round(elementLocation.y)
   };
 
   respond.send();
diff --git a/javascript/remote/ui/actiondialog.js b/javascript/remote/ui/actiondialog.js
index a9fc554..ff13269 100644
--- a/javascript/remote/ui/actiondialog.js
+++ b/javascript/remote/ui/actiondialog.js
@@ -16,7 +16,7 @@
 
 goog.require('goog.dom');
 goog.require('goog.events');
-goog.require('goog.ui.Component.EventType');
+goog.require('goog.ui.Component');
 goog.require('goog.ui.Dialog');
 
 
diff --git a/javascript/remote/ui/banner.js b/javascript/remote/ui/banner.js
index 6f461ee..c258b55 100644
--- a/javascript/remote/ui/banner.js
+++ b/javascript/remote/ui/banner.js
@@ -15,6 +15,7 @@
 goog.provide('remote.ui.Banner');
 
 goog.require('goog.dom');
+goog.require('goog.dom.TagName');
 goog.require('goog.events');
 goog.require('goog.events.EventType');
 goog.require('goog.style');
diff --git a/javascript/remote/ui/client.js b/javascript/remote/ui/client.js
index 5725f7a..6ac0f69 100644
--- a/javascript/remote/ui/client.js
+++ b/javascript/remote/ui/client.js
@@ -20,7 +20,6 @@
 goog.require('goog.array');
 goog.require('goog.debug.Console');
 goog.require('goog.debug.Logger');
-goog.require('goog.dom');
 goog.require('goog.events');
 goog.require('remote.ui.Banner');
 goog.require('remote.ui.Event.Type');
diff --git a/javascript/remote/ui/createsessiondialog.js b/javascript/remote/ui/createsessiondialog.js
index 1e35641..ea234f1 100644
--- a/javascript/remote/ui/createsessiondialog.js
+++ b/javascript/remote/ui/createsessiondialog.js
@@ -20,11 +20,11 @@
 goog.require('remote.ui.ActionDialog');
 
 
-
 /**
  * Dialog used to configure a new session request.
- * @param {!Array.<string>} browsers List of possible browsers to create
- *     sessions for.
+ * @param {!Array.<!(Object|string)>} browsers List of possible browsers to
+ *     create sessions for: each browser should be defined either by its name,
+ *     or a fully defined capabilities object.
  * @constructor
  * @extends {remote.ui.ActionDialog}
  */
@@ -32,10 +32,12 @@
   goog.base(this, 'Create a New Session');
 
   /**
-   * @type {!Array.<string>}
+   * @type {!Array.<!Object>}
    * @private
    */
-  this.browsers_ = browsers;
+  this.browsers_ = goog.array.map(browsers, function(browser) {
+    return goog.isString(browser) ? {'browserName': browser} : browser;
+  });
 
   goog.events.listen(this, goog.ui.Component.EventType.SHOW,
       this.onShow_, false, this);
@@ -71,11 +73,13 @@
   return dom.createDom(goog.dom.TagName.LABEL, null,
       'Browser:\xa0', this.browserSelect_);
 
-  function createOption(value) {
-    return dom.createDom(goog.dom.TagName.OPTION, {'value': value},
-        value.toLowerCase().replace(/\b[a-z]/g, function(c) {
-          return c.toUpperCase();
-        }));
+  function createOption(capabilities) {
+    var displayText = capabilities['browserName'];
+    var version = capabilities['version'];
+    if (version) {
+      displayText += ' ' + version;
+    }
+    return dom.createDom(goog.dom.TagName.OPTION, null, displayText);
   }
 };
 
@@ -90,13 +94,7 @@
 
 /** @override */
 remote.ui.CreateSessionDialog.prototype.getUserSelection = function() {
-  var selected = this.browserSelect_.selectedIndex;
-  return {
-    'browserName': this.browserSelect_.options[selected].value,
-    'version': '',
-    'platform': 'ANY',
-    'javascriptEnabled': true
-  };
+  return this.browsers_[this.browserSelect_.selectedIndex - 1];
 };
 
 
diff --git a/javascript/remote/ui/fieldset.js b/javascript/remote/ui/fieldset.js
index a5032c7..82babd4 100644
--- a/javascript/remote/ui/fieldset.js
+++ b/javascript/remote/ui/fieldset.js
@@ -14,6 +14,7 @@
 
 goog.provide('remote.ui.FieldSet');
 
+goog.require('goog.dom.TagName');
 goog.require('goog.ui.Component');
 
 
diff --git a/javascript/remote/ui/openscriptdialog.js b/javascript/remote/ui/openscriptdialog.js
index 5dcff40..766ec47 100644
--- a/javascript/remote/ui/openscriptdialog.js
+++ b/javascript/remote/ui/openscriptdialog.js
@@ -14,9 +14,11 @@
 
 goog.provide('remote.ui.OpenScriptDialog');
 
+goog.require('goog.dom');
 goog.require('goog.dom.TagName');
 goog.require('goog.dom.classes');
-goog.require('goog.ui.Component.EventType');
+goog.require('goog.events');
+goog.require('goog.ui.Component');
 goog.require('goog.ui.LabelInput');
 goog.require('remote.ui.ActionDialog');
 
diff --git a/javascript/remote/ui/screenshotdialog.js b/javascript/remote/ui/screenshotdialog.js
index 6f8041d..fd2316a 100644
--- a/javascript/remote/ui/screenshotdialog.js
+++ b/javascript/remote/ui/screenshotdialog.js
@@ -15,8 +15,8 @@
 goog.provide('remote.ui.ScreenshotDialog');
 goog.provide('remote.ui.ScreenshotDialog.State');
 
+goog.require('goog.dom');
 goog.require('goog.dom.TagName');
-goog.require('goog.style');
 goog.require('goog.ui.Dialog');
 
 
diff --git a/javascript/remote/ui/sessioncontainer.js b/javascript/remote/ui/sessioncontainer.js
index a480337..f6f0c69 100644
--- a/javascript/remote/ui/sessioncontainer.js
+++ b/javascript/remote/ui/sessioncontainer.js
@@ -18,11 +18,10 @@
 goog.require('goog.array');
 goog.require('goog.dom.TagName');
 goog.require('goog.events');
-goog.require('goog.events.Event');
 goog.require('goog.events.EventType');
 goog.require('goog.structs.Map');
 goog.require('goog.style');
-goog.require('goog.ui.Component.EventType');
+goog.require('goog.ui.Component');
 goog.require('goog.ui.Tab');
 goog.require('goog.ui.TabBar');
 goog.require('remote.ui.ControlBlock');
diff --git a/javascript/remote/ui/sessionview.js b/javascript/remote/ui/sessionview.js
index c2b930d..d4d8836 100644
--- a/javascript/remote/ui/sessionview.js
+++ b/javascript/remote/ui/sessionview.js
@@ -16,7 +16,7 @@
 
 goog.require('goog.dom');
 goog.require('goog.dom.TagName');
-goog.require('goog.events.Event');
+goog.require('goog.events');
 goog.require('goog.math.Box');
 goog.require('goog.style');
 goog.require('goog.ui.Button');
diff --git a/javascript/safari-driver/extension/commands.js b/javascript/safari-driver/extension/commands.js
index 7ada622..c301d87 100644
--- a/javascript/safari-driver/extension/commands.js
+++ b/javascript/safari-driver/extension/commands.js
@@ -22,6 +22,7 @@
 
 goog.require('bot.response');
 goog.require('goog.Uri');
+goog.require('goog.array');
 goog.require('goog.debug.Logger');
 goog.require('goog.string');
 goog.require('safaridriver.extension.Tab');
diff --git a/javascript/safari-driver/extension/session.js b/javascript/safari-driver/extension/session.js
index 9992c35..0f809c0 100644
--- a/javascript/safari-driver/extension/session.js
+++ b/javascript/safari-driver/extension/session.js
@@ -18,7 +18,7 @@
 
 goog.require('goog.string');
 goog.require('goog.userAgent');
-goog.require('goog.userAgent.product.isVersion');
+goog.require('goog.userAgent.product');
 goog.require('webdriver.Session');
 
 
diff --git a/javascript/safari-driver/extension/tab.js b/javascript/safari-driver/extension/tab.js
index 177a912..41cc44b 100644
--- a/javascript/safari-driver/extension/tab.js
+++ b/javascript/safari-driver/extension/tab.js
@@ -19,6 +19,7 @@
 goog.require('bot.response');
 goog.require('goog.Uri');
 goog.require('goog.asserts');
+goog.require('goog.debug.Logger');
 goog.require('safaridriver.Tab');
 goog.require('safaridriver.message.Command');
 goog.require('safaridriver.message.Load');
diff --git a/javascript/safari-driver/extension/tabmanager.js b/javascript/safari-driver/extension/tabmanager.js
index 1d1ba0f..bceea32 100644
--- a/javascript/safari-driver/extension/tabmanager.js
+++ b/javascript/safari-driver/extension/tabmanager.js
@@ -18,7 +18,6 @@
 
 goog.require('goog.array');
 goog.require('goog.debug.Logger');
-goog.require('goog.string');
 goog.require('safaridriver.extension.Tab');
 
 
diff --git a/javascript/safari-driver/externs/extension.js b/javascript/safari-driver/externs/extension.js
index ee12464..45f2be4 100644
--- a/javascript/safari-driver/externs/extension.js
+++ b/javascript/safari-driver/externs/extension.js
@@ -20,4 +20,4 @@
 
 
 /** @type {SafariNamespace} */
-var safari;
\ No newline at end of file
+var safari;
diff --git a/javascript/safari-driver/inject/pagescript.js b/javascript/safari-driver/inject/pagescript.js
index 099488c..0283b1e 100644
--- a/javascript/safari-driver/inject/pagescript.js
+++ b/javascript/safari-driver/inject/pagescript.js
@@ -18,6 +18,7 @@
 goog.require('bot.inject');
 goog.require('bot.response');
 goog.require('goog.debug.Logger');
+goog.require('goog.dom');
 goog.require('safaridriver.inject.Encoder');
 goog.require('safaridriver.inject.message');
 goog.require('safaridriver.message.Command');
diff --git a/javascript/selenium-atoms/events.js b/javascript/selenium-atoms/events.js
index 01ce888..ebb414c 100644
--- a/javascript/selenium-atoms/events.js
+++ b/javascript/selenium-atoms/events.js
@@ -31,6 +31,7 @@
 goog.require('goog.dom');
 goog.require('goog.dom.TagName');
 goog.require('goog.style');
+goog.require('goog.userAgent');
 goog.require('goog.userAgent.product');
 
 
@@ -39,6 +40,9 @@
 core.events.metaKeyDown_ = false;
 core.events.shiftKeyDown_ = false;
 
+/**
+ * @type {function(*): !Object}
+ */
 var XPCNativeWrapper = XPCNativeWrapper || function(_) {};
 
 core.events.getEventFactory_ = function(eventName) {
@@ -140,7 +144,7 @@
       metaKey: false,
       relatedTarget: null
   };
-  bot.events.fire(element, type, (/** @type{!bot.events.MouseArgs} */args));
+  bot.events.fire(element, type, (/** @type {!bot.events.MouseArgs} */args));
 };
 
 
@@ -201,8 +205,8 @@
  */
 core.events.setValue = function(locator, value) {
   if (core.events.controlKeyDown_ || core.events.altKeyDown_ || core.events.metaKeyDown_) {
-    throw new core.Error("type not supported immediately after call to " +
-        "controlKeyDown() or altKeyDown() or metaKeyDown()");
+    throw new core.Error('type not supported immediately after call to ' +
+        'controlKeyDown() or altKeyDown() or metaKeyDown()');
   }
 
   // TODO(simon): fail if it can't be typed into.
diff --git a/javascript/selenium-atoms/firefox-chrome.js b/javascript/selenium-atoms/firefox-chrome.js
index 034e244..4f6d259 100644
--- a/javascript/selenium-atoms/firefox-chrome.js
+++ b/javascript/selenium-atoms/firefox-chrome.js
@@ -1,7 +1,7 @@
 goog.provide('core.firefox');
 
 /**
- * @returns {boolean} Whether the firefox instance needs elements to be
+ * @return {boolean} Whether the firefox instance needs elements to be
  * unwrapped.
  */
 core.firefox.isUsingUnwrapping_ = function() {
@@ -12,7 +12,7 @@
                 getService(Components.interfaces.nsIVersionComparator);
 
         return (versionChecker.compare(appInfo.version, '4.0') >= 0);
-    } catch(e) {
+    } catch (e) {
         // like when its not Firefox
         return false;
     }
@@ -25,7 +25,7 @@
  * Unwraps a something which is wrapped into a XPCNativeWrapper or XrayWrapper.
  *
  * @param {!Object} thing The "something" to unwrap.
- * @returns {!Object} The object, unwrapped if possible.
+ * @return {!Object} The object, unwrapped if possible.
  */
 core.firefox.unwrap = function(thing) {
     if (!core.firefox.isUsingUnwrapping_) {
@@ -61,7 +61,7 @@
             toReturn.__fxdriver_unwrapped = true;
             return toReturn;
         }
-    } catch(e) {
+    } catch (e) {
         // Unwrapping will fail for JS literals - numbers, for example. Catch
         // the exception and proceed, it will eventually be returned as-is.
     }
diff --git a/javascript/selenium-atoms/test/event_firing_test.html b/javascript/selenium-atoms/test/event_firing_test.html
index 49c305c..47680a1 100644
--- a/javascript/selenium-atoms/test/event_firing_test.html
+++ b/javascript/selenium-atoms/test/event_firing_test.html
@@ -18,7 +18,8 @@
         // These should work, they just dosn't.
         var CLICKING_WITH_COORDINATES_BROKEN = goog.userAgent.product.SAFARI ||
             goog.userAgent.product.ANDROID ||
-            goog.userAgent.product.OPERA;
+            (goog.userAgent.product.OPERA &&
+             bot.userAgent.isEngineVersion(12));
 
         function setUp() {
           fired = false;
diff --git a/javascript/selenium-atoms/text.js b/javascript/selenium-atoms/text.js
index 9311c7c..88bf79e 100644
--- a/javascript/selenium-atoms/text.js
+++ b/javascript/selenium-atoms/text.js
@@ -150,7 +150,7 @@
   var text = '';
   var isRecentFirefox =
       (goog.userAgent.GECKO && goog.userAgent.VERSION >= '1.8');
-  
+
   if (isRecentFirefox ||
       goog.userAgent.SAFARI || goog.userAgent.OPERA || goog.userAgent.IE) {
     text = core.text.getTextContent_(element, false);
@@ -191,9 +191,9 @@
   var allText = core.text.getBodyText();
 
   var patternMatcher = core.patternMatcher.against(pattern);
-  if (patternMatcher.strategyName == "glob") {
-    if (pattern.indexOf("glob:") == 0) {
-      pattern = pattern.substring("glob:".length); // strip off "glob:"
+  if (patternMatcher.strategyName == 'glob') {
+    if (pattern.indexOf('glob:') == 0) {
+      pattern = pattern.substring('glob:'.length); // strip off "glob:"
     }
     patternMatcher = core.patternMatcher.against('globContains:' + pattern);
   }
diff --git a/javascript/webdriver/abstractbuilder.js b/javascript/webdriver/abstractbuilder.js
index 7f1d65b..76d2d21 100644
--- a/javascript/webdriver/abstractbuilder.js
+++ b/javascript/webdriver/abstractbuilder.js
@@ -171,4 +171,4 @@
  * current configuration.
  * @return {!webdriver.WebDriver} A new WebDriver client.
  */
-webdriver.AbstractBuilder.prototype.build = goog.abstractMethod;
\ No newline at end of file
+webdriver.AbstractBuilder.prototype.build = goog.abstractMethod;
diff --git a/javascript/webdriver/actionsequence.js b/javascript/webdriver/actionsequence.js
index e420ef5..6df1d94 100644
--- a/javascript/webdriver/actionsequence.js
+++ b/javascript/webdriver/actionsequence.js
@@ -353,4 +353,4 @@
 webdriver.ActionSequence.prototype.sendKeys = function(var_args) {
   var keys = goog.array.flatten(goog.array.slice(arguments, 0));
   return this.scheduleKeyboardAction_('sendKeys', keys);
-};
\ No newline at end of file
+};
diff --git a/javascript/webdriver/atoms/element.js b/javascript/webdriver/atoms/element.js
index 4a3ea4b..2447bfa 100644
--- a/javascript/webdriver/atoms/element.js
+++ b/javascript/webdriver/atoms/element.js
@@ -47,6 +47,75 @@
 
 
 /**
+ * Common aliases for properties. This maps names that users use to the correct
+ * property name.
+ *
+ * @const
+ * @private
+ */
+webdriver.atoms.element.PROPERTY_ALIASES_ = {
+  'class': 'className',
+  'readonly': 'readOnly'
+};
+
+
+/**
+ * Used to determine whether we should return a boolean value from getAttribute.
+ * These are all extracted from the WHATWG spec:
+ *
+ *   http://www.whatwg.org/specs/web-apps/current-work/
+ *
+ * These must all be lower-case.
+ *
+ * @const
+ * @private
+ */
+webdriver.atoms.element.BOOLEAN_PROPERTIES_ = [
+  'async',
+  'autofocus',
+  'autoplay',
+  'checked',
+  'compact',
+  'complete',
+  'controls',
+  'declare',
+  'defaultchecked',
+  'defaultselected',
+  'defer',
+  'disabled',
+  'draggable',
+  'ended',
+  'formnovalidate',
+  'hidden',
+  'indeterminate',
+  'iscontenteditable',
+  'ismap',
+  'itemscope',
+  'loop',
+  'multiple',
+  'muted',
+  'nohref',
+  'noresize',
+  'noshade',
+  'novalidate',
+  'nowrap',
+  'open',
+  'paused',
+  'pubdate',
+  'readonly',
+  'required',
+  'reversed',
+  'scoped',
+  'seamless',
+  'seeking',
+  'selected',
+  'spellcheck',
+  'truespeed',
+  'willvalidate'
+];
+
+
+/**
  * Get the value of the given property or attribute. If the "attribute" is for
  * a boolean property, we return null in the case where the value is false. If
  * the attribute name is "style" an attempt to convert that style into a string
@@ -70,7 +139,7 @@
     return (/** @type {?string} */value);
   }
 
-  if ('selected' == name || 'checked' == name &&
+  if (('selected' == name || 'checked' == name) &&
       bot.dom.isSelectable(element)) {
     return bot.dom.isSelected(element) ? 'true' : null;
   }
@@ -92,15 +161,17 @@
     return (/** @type {?string} */value);
   }
 
-  if (bot.dom.isBooleanAttribute(attribute.toLowerCase())) {
+  var propName = webdriver.atoms.element.PROPERTY_ALIASES_[attribute] ||
+      attribute;
+  if (goog.array.contains(webdriver.atoms.element.BOOLEAN_PROPERTIES_, name)) {
     value = bot.dom.getAttribute(element, attribute) ||
-        bot.dom.getProperty(element, attribute);
+        bot.dom.getProperty(element, propName);
     return value ? 'true' : null;
   }
 
   var property;
   try {
-    property = bot.dom.getProperty(element, attribute);
+    property = bot.dom.getProperty(element, propName);
   } catch (e) {
     // Leaves property undefined or null
   }
@@ -170,7 +241,7 @@
   if (webdriver.atoms.element.isInHead_(element)) {
     var doc = goog.dom.getOwnerDocument(element);
     if (element.tagName.toUpperCase() == goog.dom.TagName.TITLE &&
-        goog.dom.getWindow(doc) == bot.window_.top) {
+        goog.dom.getWindow(doc) == bot.getWindow().top) {
       return goog.string.trim((/** @type {string} */doc.title));
     }
     return '';
diff --git a/javascript/webdriver/atoms/inject/action.js b/javascript/webdriver/atoms/inject/action.js
index 346d577..fcfb049 100644
--- a/javascript/webdriver/atoms/inject/action.js
+++ b/javascript/webdriver/atoms/inject/action.js
@@ -20,9 +20,9 @@
 goog.provide('webdriver.atoms.inject.action');
 
 goog.require('bot.action');
-goog.require('bot.inject');
 goog.require('goog.dom.selection');
 goog.require('webdriver.atoms.element');
+goog.require('webdriver.atoms.inject');
 
 
 /**
@@ -33,8 +33,8 @@
  * @return {string} A stringified {@link bot.response.ResponseObject}.
  */
 webdriver.atoms.inject.action.type = function(element, keys) {
-  return bot.inject.executeScript(webdriver.atoms.element.type,
-      [element, keys], true);
+  return webdriver.atoms.inject.executeScript(webdriver.atoms.element.type,
+      [element, keys]);
 };
 
 
@@ -45,7 +45,7 @@
  * @return {string} A stringified {@link bot.response.ResponseObject}.
  */
 webdriver.atoms.inject.action.submit = function(element) {
-  return bot.inject.executeScript(bot.action.submit, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.action.submit, [element]);
 };
 
 
@@ -57,7 +57,7 @@
  * @see bot.action.clear
  */
 webdriver.atoms.inject.action.clear = function(element) {
-  return bot.inject.executeScript(bot.action.clear, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.action.clear, [element]);
 };
 
 
@@ -69,6 +69,6 @@
  * @see bot.action.click
  */
 webdriver.atoms.inject.action.click = function(element) {
-  return bot.inject.executeScript(bot.action.click, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.action.click, [element]);
 };
 
diff --git a/javascript/webdriver/atoms/inject/appcache.js b/javascript/webdriver/atoms/inject/appcache.js
index a846dae..610a792 100644
--- a/javascript/webdriver/atoms/inject/appcache.js
+++ b/javascript/webdriver/atoms/inject/appcache.js
@@ -25,11 +25,9 @@
 /**
  * Gets the status of the application cache.
  *
- * @return {number} The status of the application cache.
+ * @return {string} The status of the application cache.
  */
 webdriver.atoms.inject.storage.appcache.getStatus = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.appcache.getStatus,
-      [], true);
+  return /**@type {string}*/(bot.inject.executeScript(
+      webdriver.atoms.storage.appcache.getStatus, [], true));
 };
-
-
diff --git a/javascript/webdriver/atoms/inject/dom.js b/javascript/webdriver/atoms/inject/dom.js
index 026c972..cf5743f 100644
--- a/javascript/webdriver/atoms/inject/dom.js
+++ b/javascript/webdriver/atoms/inject/dom.js
@@ -14,101 +14,104 @@
 // limitations under the License.
 
 /**
- *@fileoverview Ready to inject atoms for querying the DOM.
+ * @fileoverview Ready to inject atoms for querying the DOM.
  */
 
 goog.provide('webdriver.atoms.inject.dom');
 
 goog.require('bot.action');
 goog.require('bot.dom');
-goog.require('bot.inject');
 goog.require('webdriver.atoms.element');
+goog.require('webdriver.atoms.inject');
 
 
 /**
  * Gets the visisble text for the given element.
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @return {string} The visible text wrapped in a JSON string as defined by the
  *     WebDriver wire protocol.
  */
 webdriver.atoms.inject.dom.getText = function(element) {
-  return bot.inject.executeScript(webdriver.atoms.element.getText, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.getVisibleText,
+      [element]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @return {string} A boolean describing whether the element is
  *     checked or selected wrapped in a JSON string as defined by
  *     the wire protocol.
  */
 webdriver.atoms.inject.dom.isSelected = function(element) {
-  return bot.inject.executeScript(bot.dom.isSelected, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.isSelected, [element]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @return {string} The coordinates of the top left corner in a JSON
  *     string as defined by the wire protocol.
  */
 webdriver.atoms.inject.dom.getTopLeftCoordinates = function(element) {
-  return bot.inject.executeScript(bot.dom.getLocationInView, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.getLocationInView,
+      [element]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @param {string} attribute The attribute to look up.
  * @return {string} The requested attribute value in a JSON string
  *     as defined by the wire protocol.
  */
 webdriver.atoms.inject.dom.getAttributeValue = function(element, attribute) {
-  return bot.inject.executeScript(
-      webdriver.atoms.element.getAttribute, [element, attribute], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.element.getAttribute, [element, attribute]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @return {string} The element size in a JSON string as
  *     defined by the wire protocol.
  */
 webdriver.atoms.inject.dom.getSize = function(element) {
-  return bot.inject.executeScript(bot.dom.getElementSize, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.getElementSize,
+      [element]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @param {string} property The property to look up.
  * @return {string} The value of the requested CSS property in a JSON
  *     string as defined by the wire protocol.
  */
-webdriver.atoms.inject.dom.getValueOfCssProperty = function(element, property) {
-  return bot.inject.executeScript(bot.dom.getEffectiveStyle,
-      [element, property], true);
+webdriver.atoms.inject.dom.getValueOfCssProperty =
+    function(element, property) {
+  return webdriver.atoms.inject.executeScript(bot.dom.getEffectiveStyle,
+      [element, property]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to query.
  * @return {string} A boolean describing whether the element is enabled
  *     in a JSON string as defined by the wire protocol.
  */
 webdriver.atoms.inject.dom.isEnabled = function(element) {
-  return bot.inject.executeScript(bot.dom.isEnabled, [element], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.isEnabled, [element]);
 };
 
 
 /**
- * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to check.
+ * @param {{bot.inject.ELEMENT_KEY: string}} element The element to check.
  * @return {string} true if the element is visisble, false otherwise.
  *     The result is wrapped in a JSON string as defined by the wire
  *     protocol.
  */
 webdriver.atoms.inject.dom.isDisplayed = function(element) {
-  return bot.inject.executeScript(
-      bot.dom.isShown, [element, /*ignoreOpacity=*/true], true);
+  return webdriver.atoms.inject.executeScript(bot.dom.isShown,
+      [element, /*ignoreOpacity=*/true]);
 };
-
diff --git a/javascript/webdriver/atoms/inject/execute_script.js b/javascript/webdriver/atoms/inject/execute_script.js
index f1bd679..4c6f8f4 100644
--- a/javascript/webdriver/atoms/inject/execute_script.js
+++ b/javascript/webdriver/atoms/inject/execute_script.js
@@ -31,31 +31,30 @@
  * @param {Array.<*>} args Array of arguments to pass to fn.
  * @param {{bot.inject.WINDOW_KEY:string}=} opt_window The serialized window
  *     object to be read from the cache.
- * @return {!(string|bot.response.ResponseObject)} The response object. If
- *     opt_stringify is true, the result will be serialized and returned in
- *     string format.
+ * @return {string} The response object, serialized and returned in string
+ *     format.
  */
 webdriver.atoms.inject.executeScript = function(fn, args, opt_window) {
-  return bot.inject.executeScript(
-      fn, args, true, webdriver.atoms.inject.getWindow_(opt_window));
+  return /**@type {string}*/(bot.inject.executeScript(fn, args, true,
+      webdriver.atoms.inject.getWindow_(opt_window)));
 };
 
 
 /**
  *
- * @param {!(string|function)} fn The function to execute.
+ * @param {!(string|Function)} fn The function to execute.
  * @param {Array.<*>} args Array of arguments to pass to fn.
- * @param {int} timeout The timeout to wait up to in millis.
+ * @param {number} timeout The timeout to wait up to in millis.
  * @param {{bot.inject.WINDOW_KEY:string}=} opt_window The serialized window
  *     object to be read from the cache.
- * @return {!(string|bot.response.ResponseObject)} The response object. If
- *     opt_stringify is true, the result will be serialized and returned in
- *     string format.
+ * @return {string} The response object, serialized and returned in string
+ *     format.
  */
 webdriver.atoms.inject.executeAsyncScript =
     function(fn, args, timeout, onDone, opt_window) {
-  return bot.inject.executeScript(fn, args, timeout, onDone, true,
-      webdriver.atoms.inject.getWindow_(opt_window));
+  return /** @type {string} */(bot.inject.executeAsyncScript(
+      fn, args, timeout, onDone, true,
+      webdriver.atoms.inject.getWindow_(opt_window)));
 };
 
 
@@ -74,5 +73,5 @@
   } else {
     win = window;
   }
-  return win;
+  return /**@type {!Window}*/(win);
 };
diff --git a/javascript/webdriver/atoms/inject/find_element.js b/javascript/webdriver/atoms/inject/find_element.js
index 8ab2597..441717a 100644
--- a/javascript/webdriver/atoms/inject/find_element.js
+++ b/javascript/webdriver/atoms/inject/find_element.js
@@ -19,9 +19,8 @@
 
 goog.provide('webdriver.atoms.inject.locators');
 
-goog.require('bot.inject');
 goog.require('bot.locators');
-
+goog.require('webdriver.atoms.inject');
 
 /**
  * Finds an element by using the given lookup strategy.
@@ -33,12 +32,12 @@
  * @return {string} The result wrapped
  *     in a JSON string as defined by the WebDriver wire protocol.
  */
-webdriver.atoms.inject.locators.findElement = function(strategy, using,
-    opt_root) {
+webdriver.atoms.inject.locators.findElement =
+    function(strategy, using, opt_root) {
   var locator = {};
   locator[strategy] = using;
-  return bot.inject.executeScript(bot.locators.findElement,
-      [locator, opt_root], true);
+  return webdriver.atoms.inject.executeScript(bot.locators.findElement,
+      [locator, opt_root]);
 };
 
 
@@ -52,10 +51,10 @@
  * @return {string} The result wrapped
  *     in a JSON string as defined by the WebDriver wire protocol.
  */
-webdriver.atoms.inject.locators.findElements = function(strategy, using,
-    opt_root) {
+webdriver.atoms.inject.locators.findElements =
+    function(strategy, using, opt_root) {
   var locator = {};
   locator[strategy] = using;
-  return bot.inject.executeScript(bot.locators.findElements,
-                                  [locator, opt_root], true);
+  return webdriver.atoms.inject.executeScript(bot.locators.findElements,
+      [locator, opt_root]);
 };
diff --git a/javascript/webdriver/atoms/inject/frame.js b/javascript/webdriver/atoms/inject/frame.js
index dd3ca47..03ec6df 100644
--- a/javascript/webdriver/atoms/inject/frame.js
+++ b/javascript/webdriver/atoms/inject/frame.js
@@ -20,8 +20,8 @@
 goog.provide('webdriver.atoms.inject.frame');
 
 goog.require('bot.frame');
-goog.require('bot.inject');
 goog.require('bot.inject.cache');
+goog.require('webdriver.atoms.inject');
 
 
 /**
@@ -33,10 +33,10 @@
  * @return {string} A frame element wrapped in a JSON string as defined by
  *     the wire protocol.
  */
-webdriver.atoms.inject.frame.findFrameByIdOrName = function(idOrName,
-    opt_root) {
-  return bot.inject.executeScript(bot.frame.findFrameByNameOrId,
-      [idOrName, opt_root], true);
+webdriver.atoms.inject.frame.findFrameByIdOrName =
+    function(idOrName, opt_root) {
+  return webdriver.atoms.inject.executeScript(bot.frame.findFrameByNameOrId,
+      [idOrName, opt_root]);
 };
 
 
@@ -44,7 +44,7 @@
  * @return {string} A string representing the currently active element.
  */
 webdriver.atoms.inject.frame.activeElement = function() {
-  return bot.inject.executeScript(bot.frame.activeElement, [], true);
+  return webdriver.atoms.inject.executeScript(bot.frame.activeElement, []);
 };
 
 
@@ -58,8 +58,8 @@
  *     the wire protocol.
  */
 webdriver.atoms.inject.frame.findFrameByIndex = function(index, opt_root) {
-  return bot.inject.executeScript(bot.frame.findFrameByIndex,
-      [index, opt_root], true);
+  return webdriver.atoms.inject.executeScript(bot.frame.findFrameByIndex,
+      [index, opt_root]);
 };
 
 
@@ -68,17 +68,16 @@
  *     which is the top window.
  */
 webdriver.atoms.inject.frame.defaultContent = function() {
-  return bot.inject.executeScript(bot.frame.defaultContent,
-      [], true);
+  return webdriver.atoms.inject.executeScript(bot.frame.defaultContent, []);
 };
 
 
 /**
  * @param {!{bot.inject.ELEMENT_KEY:string}} element The element to query.
  * @return {string} The window corresponding to the frame element
- * wrapped in a JSON string as defined by the wire protocol.
+ *     wrapped in a JSON string as defined by the wire protocol.
  */
 webdriver.atoms.inject.frame.getFrameWindow = function(element) {
-  return bot.inject.executeScript(bot.frame.getFrameWindow,
-      [element], true);
+  return webdriver.atoms.inject.executeScript(bot.frame.getFrameWindow,
+      [element]);
 };
diff --git a/javascript/webdriver/atoms/inject/local_storage.js b/javascript/webdriver/atoms/inject/local_storage.js
index 8dd258f..69b7f72 100644
--- a/javascript/webdriver/atoms/inject/local_storage.js
+++ b/javascript/webdriver/atoms/inject/local_storage.js
@@ -19,7 +19,7 @@
 
 goog.provide('webdriver.atoms.inject.storage.local');
 
-goog.require('bot.inject');
+goog.require('webdriver.atoms.inject');
 goog.require('webdriver.atoms.storage.local');
 
 
@@ -28,12 +28,12 @@
  *
  * @param {string} key The key of the item.
  * @param {*} value The value of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.setItem = function(key, value) {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.setItem,
-      [key, value], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.setItem, [key, value]);
 };
 
 
@@ -41,24 +41,24 @@
  * Gets an item from the local storage.
  *
  * @param {string} key The key of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.getItem = function(key) {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.getItem,
-      [key], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.getItem, [key]);
 };
 
 
 /**
  * Gets the key set of the entries.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.keySet = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.keySet,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.keySet, []);
 };
 
 
@@ -66,34 +66,34 @@
  * Removes an item in the local storage.
  *
  * @param {string} key The key of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.removeItem = function(key) {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.removeItem,
-      [key], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.removeItem, [key]);
 };
 
 
 /**
  * Clears the local storage.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.clear = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.clear,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.clear, []);
 };
 
 
 /**
  * Gets the size of the local storage.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.local.size = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.local.size,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.local.size, []);
 };
diff --git a/javascript/webdriver/atoms/inject/session_storage.js b/javascript/webdriver/atoms/inject/session_storage.js
index bc63e63..462041c 100644
--- a/javascript/webdriver/atoms/inject/session_storage.js
+++ b/javascript/webdriver/atoms/inject/session_storage.js
@@ -19,7 +19,7 @@
 
 goog.provide('webdriver.atoms.inject.storage.session');
 
-goog.require('bot.inject');
+goog.require('webdriver.atoms.inject');
 goog.require('webdriver.atoms.storage.session');
 
 
@@ -28,12 +28,12 @@
  *
  * @param {string} key The key of the item.
  * @param {*} value The value of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.setItem = function(key, value) {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.setItem,
-      [key, value], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.setItem, [key, value]);
 };
 
 
@@ -41,24 +41,24 @@
  * Gets an item from the session storage.
  *
  * @param {string} key The key of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.getItem = function(key) {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.getItem,
-      [key], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.getItem, [key]);
 };
 
 
 /**
  * Gets the key set of the entries.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.keySet = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.keySet,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.keySet, []);
 };
 
 
@@ -66,34 +66,34 @@
  * Removes an item in the session storage.
  *
  * @param {string} key The key of the item.
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.removeItem = function(key) {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.removeItem,
-      [key], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.removeItem, [key]);
 };
 
 
 /**
  * Clears the session storage.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.clear = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.clear,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.clear, []);
 };
 
 
 /**
  * Gets the size of the session storage.
  *
- * @return {!bot.response.ResponseObject} The result wrapped according
- *     to the wire protocol.
+ * @return {string} The stringified result wrapped according to the wire
+ *     protocol.
  */
 webdriver.atoms.inject.storage.session.size = function() {
-  return bot.inject.executeScript(webdriver.atoms.storage.session.size,
-      [], true);
+  return webdriver.atoms.inject.executeScript(
+      webdriver.atoms.storage.session.size, []);
 };
diff --git a/javascript/webdriver/atoms/inject/sql_database.js b/javascript/webdriver/atoms/inject/sql_database.js
index b531eb4..4831720 100644
--- a/javascript/webdriver/atoms/inject/sql_database.js
+++ b/javascript/webdriver/atoms/inject/sql_database.js
@@ -21,8 +21,8 @@
 
 goog.require('bot.Error');
 goog.require('bot.ErrorCode');
-goog.require('bot.inject');
 goog.require('bot.storage.database');
+goog.require('webdriver.atoms.inject');
 
 
 /**
@@ -31,23 +31,23 @@
  * @param {string} databaseName The name of the database.
  * @param {string} query The SQL statement.
  * @param {Array.<*>} args Arguments to pass to the query.
- * @param {!function(string)} onDone The callback to invoke when done. The
+ * @param {function(string)} onDone The callback to invoke when done. The
  *     result, according to the wire protocol, will be passed to this callback.
  */
 webdriver.atoms.inject.storage.database.executeSql =
     function(databaseName, query, args, onDone) {
   var onSuccessCallback = function(tx, result) {
-    onDone(bot.inject.executeScript(function(res) {
+    onDone(webdriver.atoms.inject.executeScript(function(res) {
       return result;
-    }, [result], true));
+    }, [result]));
   };
 
   var onErrorCallback = function(error) {
-    onDone(bot.inject.executeScript(function() {
+    onDone(webdriver.atoms.inject.executeScript(function() {
       throw new bot.Error(bot.ErrorCode.SQL_DATABASE_ERROR,
           'SQL Error Code: ' + error.code + '. SQL Error Message: ' +
           error.message);
-    }, [], true));
+    }, []));
   };
 
   bot.storage.database.executeSql(
diff --git a/javascript/webdriver/atoms/inputs.js b/javascript/webdriver/atoms/inputs.js
index bfbb745..75fec8e 100644
--- a/javascript/webdriver/atoms/inputs.js
+++ b/javascript/webdriver/atoms/inputs.js
@@ -1,183 +1,191 @@
-// Copyright 2012 WebDriver committers

-// Copyright 2012 Software Freedom Conservancy

-//

-// Licensed under the Apache License, Version 2.0 (the "License");

-// you may not use this file except in compliance with the License.

-// You may obtain a copy of the License at

-//

-//      http://www.apache.org/licenses/LICENSE-2.0

-//

-// Unless required by applicable law or agreed to in writing, software

-// distributed under the License is distributed on an "AS IS" BASIS,

-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

-// See the License for the specific language governing permissions and

-// limitations under the License.

-

-/**

- * @fileoverview Synthetic events for fun and profit.

- */

-

-goog.provide('webdriver.atoms.inputs');

-

-goog.require('bot.Keyboard');

-goog.require('bot.Mouse');

-goog.require('bot.action');

-goog.require('goog.array');

-goog.require('webdriver.atoms.element');

-

-

-/**

- * Send keyboard input to a particular element.

- *

- * @param {!Element} element The element to send the keyboard input to.

- * @param {Array.<!bot.Keyboard.Key>=} opt_state The keyboard to use, or

- *     construct one.

- * @param {...(string|!Array.<string>)} var_args What to type.

- * @return {Array.<!bot.Keyboard.Key>} The keyboard state.

- */

-webdriver.atoms.inputs.sendKeys = function(element, opt_state, var_args) {

-  var keyboard = new bot.Keyboard(opt_state);

-  var to_type = goog.array.slice(arguments, 2);

-  var flattened = goog.array.flatten(to_type);

-  if (!element) {

-    element = bot.dom.getActiveElement(document);

-  }

-  webdriver.atoms.element.type(element, flattened, keyboard);

-

-  return keyboard.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.sendKeys',

-                  webdriver.atoms.inputs.sendKeys);

-

-

-/**

- * Click on an element.

- *

- * @param {!Element} element The element to click.

- * @param {Object=} opt_state The serialized state of the mouse.

- * @return {!bot.Mouse.State} The mouse state.

- * @return {Object} The mouse state.

- */

-webdriver.atoms.inputs.click = function(element, opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  if (!element) {

-    element = mouse.getElement();

-  }

-  bot.action.click(element, null, mouse);

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.click',

-                  webdriver.atoms.inputs.click);

-

-

-/**

- * Move the mouse to a specific element and/or coordinate location.

- *

- * @param {!Element} element The element to move the mouse to.

- * @param {number} x_offset The x coordinate to use as an offset.

- * @param {number} y_offset The y coordinate to use as an offset.

- * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.

- * @return {!bot.Mouse.State} The mouse state.

- */

-webdriver.atoms.inputs.mouseMove = function(element, x_offset, y_offset,

-    opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  var target = element || mouse.getElement();

-

-  var offset_specified = (x_offset != null) && (y_offset != null);

-  x_offset = x_offset || 0;

-  y_offset = y_offset || 0;

-

-  // If we have specified an element and no offset, we should

-  // move the mouse to the center of the specified element.

-  if (element) {

-    if (!offset_specified) {

-      var source_element_size = bot.action.getInteractableSize_(element);

-      x_offset = Math.floor(source_element_size.width / 2);

-      y_offset = Math.floor(source_element_size.height / 2);

-    }

-  } else {

-    // Moving to an absolute offset from the current target element,

-    // so we have to account for the existing offset of the current

-    // mouse position to the element origin (upper-left corner).

-    var pos = goog.style.getClientPosition(target);

-    x_offset += (mouse.clientXY_.x - pos.x);

-    y_offset += (mouse.clientXY_.y - pos.y);

-  }

-

-  var doc = goog.dom.getOwnerDocument(target);

-  var win = goog.dom.getWindow(doc);

-  var inViewAfterScroll = bot.action.scrollIntoView(

-      target,

-      new goog.math.Coordinate(x_offset, y_offset));

-

-  var coords = new goog.math.Coordinate(x_offset, y_offset);

-  mouse.move(target, coords);

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.mouseMove',

-                  webdriver.atoms.inputs.mouseMove);

-

-

-/**

- * Presses the primary mouse button at the current location.

- *

- * @param {Object=} opt_state The serialized state of the mouse.

- * @return {Object} The mouse state.

- */

-webdriver.atoms.inputs.mouseButtonDown = function(opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  mouse.pressButton(bot.Mouse.Button.LEFT);

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.mouseButtonDown',

-                  webdriver.atoms.inputs.mouseButtonDown);

-

-

-/**

- * Releases the primary mouse button at the current location.

- *

- * @param {Object=} opt_state The serialized state of the mouse.

- * @return {Object} The mouse state.

- */

-webdriver.atoms.inputs.mouseButtonUp = function(opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  mouse.releaseButton();

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.mouseButtonUp',

-                  webdriver.atoms.inputs.mouseButtonUp);

-

-

-/**

- * Double-clicks primary mouse button at the current location.

- *

- * @param {Object=} opt_state The serialized state of the mouse.

- * @return {Object} The mouse state.

- */

-webdriver.atoms.inputs.doubleClick = function(opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  mouse.pressButton(bot.Mouse.Button.LEFT);

-  mouse.releaseButton();

-  mouse.pressButton(bot.Mouse.Button.LEFT);

-  mouse.releaseButton();

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.doubleClick',

-                  webdriver.atoms.inputs.doubleClick);

-

-

-/**

- * Right-clicks mouse button at the current location.

- *

- * @param {Object=} opt_state The serialized state of the mouse.

- * @return {Object} The mouse state.

- */

-webdriver.atoms.inputs.rightClick = function(opt_state) {

-  var mouse = new bot.Mouse(opt_state);

-  mouse.pressButton(bot.Mouse.Button.RIGHT);

-  mouse.releaseButton();

-  return mouse.getState();

-};

-goog.exportSymbol('webdriver.atoms.inputs.rightClick',

-                  webdriver.atoms.inputs.rightClick);

+// Copyright 2012 WebDriver committers
+// Copyright 2012 Software Freedom Conservancy
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Synthetic events for fun and profit.
+ */
+
+goog.provide('webdriver.atoms.inputs');
+
+goog.require('bot.Keyboard');
+goog.require('bot.Mouse');
+goog.require('bot.action');
+goog.require('goog.array');
+goog.require('goog.dom');
+goog.require('goog.math.Coordinate');
+goog.require('goog.style');
+goog.require('webdriver.atoms.element');
+
+
+/**
+ * Send keyboard input to a particular element.
+ *
+ * @param {Element} element The element to send the keyboard input to.
+ * @param {Array.<!bot.Keyboard.Key>=} opt_state The keyboard to use, or
+ *     construct one.
+ * @param {...(string|!Array.<string>)} var_args What to type.
+ * @return {Array.<!bot.Keyboard.Key>} The keyboard state.
+ */
+webdriver.atoms.inputs.sendKeys = function(element, opt_state, var_args) {
+  var keyboard = new bot.Keyboard(opt_state);
+  var to_type = goog.array.slice(arguments, 2);
+  var flattened = goog.array.flatten(to_type);
+  if (!element) {
+    element = bot.dom.getActiveElement(document);
+  }
+  if (!element) {
+    throw Error('No element to send keys to');
+  }
+  webdriver.atoms.element.type(element, flattened, keyboard);
+
+  return keyboard.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.sendKeys',
+                  webdriver.atoms.inputs.sendKeys);
+
+
+/**
+ * Click on an element.
+ *
+ * @param {Element} element The element to click.
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.click = function(element, opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  if (!element) {
+    element = mouse.getState().element;
+  }
+  if (!element) {
+    throw Error('No element to send keys to');
+  }
+  bot.action.click(element, null, mouse);
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.click',
+                  webdriver.atoms.inputs.click);
+
+
+/**
+ * Move the mouse to a specific element and/or coordinate location.
+ *
+ * @param {!Element} element The element to move the mouse to.
+ * @param {number} x_offset The x coordinate to use as an offset.
+ * @param {number} y_offset The y coordinate to use as an offset.
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.mouseMove = function(element, x_offset, y_offset,
+    opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  var target = element || mouse.getState().element;
+
+  var offset_specified = (x_offset != null) && (y_offset != null);
+  x_offset = x_offset || 0;
+  y_offset = y_offset || 0;
+
+  // If we have specified an element and no offset, we should
+  // move the mouse to the center of the specified element.
+  if (element) {
+    if (!offset_specified) {
+      var source_element_size = bot.action.getInteractableSize(element);
+      x_offset = Math.floor(source_element_size.width / 2);
+      y_offset = Math.floor(source_element_size.height / 2);
+    }
+  } else {
+    // Moving to an absolute offset from the current target element,
+    // so we have to account for the existing offset of the current
+    // mouse position to the element origin (upper-left corner).
+    var pos = goog.style.getClientPosition(target);
+    x_offset += (mouse.getState().clientXY.x - pos.x);
+    y_offset += (mouse.getState().clientXY.y - pos.y);
+  }
+
+  var doc = goog.dom.getOwnerDocument(target);
+  var win = goog.dom.getWindow(doc);
+  var inViewAfterScroll = bot.action.scrollIntoView(
+      target,
+      new goog.math.Coordinate(x_offset, y_offset));
+
+  var coords = new goog.math.Coordinate(x_offset, y_offset);
+  mouse.move(target, coords);
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.mouseMove',
+                  webdriver.atoms.inputs.mouseMove);
+
+
+/**
+ * Presses the primary mouse button at the current location.
+ *
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.mouseButtonDown = function(opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  mouse.pressButton(bot.Mouse.Button.LEFT);
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.mouseButtonDown',
+                  webdriver.atoms.inputs.mouseButtonDown);
+
+
+/**
+ * Releases the primary mouse button at the current location.
+ *
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.mouseButtonUp = function(opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  mouse.releaseButton();
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.mouseButtonUp',
+                  webdriver.atoms.inputs.mouseButtonUp);
+
+
+/**
+ * Double-clicks primary mouse button at the current location.
+ *
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.doubleClick = function(opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  mouse.pressButton(bot.Mouse.Button.LEFT);
+  mouse.releaseButton();
+  mouse.pressButton(bot.Mouse.Button.LEFT);
+  mouse.releaseButton();
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.doubleClick',
+                  webdriver.atoms.inputs.doubleClick);
+
+
+/**
+ * Right-clicks mouse button at the current location.
+ *
+ * @param {bot.Mouse.State=} opt_state The serialized state of the mouse.
+ * @return {!bot.Mouse.State} The mouse state.
+ */
+webdriver.atoms.inputs.rightClick = function(opt_state) {
+  var mouse = new bot.Mouse(opt_state);
+  mouse.pressButton(bot.Mouse.Button.RIGHT);
+  mouse.releaseButton();
+  return mouse.getState();
+};
+goog.exportSymbol('webdriver.atoms.inputs.rightClick',
+                  webdriver.atoms.inputs.rightClick);
diff --git a/javascript/webdriver/atoms/storage/appcache.js b/javascript/webdriver/atoms/storage/appcache.js
index 1d7f045..a9b7e62 100644
--- a/javascript/webdriver/atoms/storage/appcache.js
+++ b/javascript/webdriver/atoms/storage/appcache.js
@@ -31,4 +31,3 @@
 webdriver.atoms.storage.appcache.getStatus = function() {
   return bot.appcache.getStatus();
 };
-
diff --git a/javascript/webdriver/events.js b/javascript/webdriver/events.js
index 2957e3a..951cc5c 100644
--- a/javascript/webdriver/events.js
+++ b/javascript/webdriver/events.js
@@ -115,7 +115,7 @@
  * @return {!webdriver.EventEmitter} A self reference.
  */
 webdriver.EventEmitter.prototype.addListener = function(type, listenerFn,
-                                                        opt_scope) {
+    opt_scope) {
   return this.addListener_(type, listenerFn, opt_scope);
 };
 
diff --git a/javascript/webdriver/test/atoms/element_test.html b/javascript/webdriver/test/atoms/element_test.html
index 8d37570..c506a1d 100644
--- a/javascript/webdriver/test/atoms/element_test.html
+++ b/javascript/webdriver/test/atoms/element_test.html
@@ -50,6 +50,13 @@
       assertEquals('2', webdriver.atoms.element.getAttribute(element, 'rows'));
     }
 
+    function testShouldReturnSameValueForClassAndClassName() {
+      var element = document.body;
+
+      assertEquals(webdriver.atoms.element.getAttribute(element, 'class'),
+                   webdriver.atoms.element.getAttribute(element, 'className'));
+    }
+
     function testShouldReturnBooleanPropertiesAsStrings() {
       var readOnlyElement = bot.locators.findElement({id: 'ro'}),
             normalElement = bot.locators.findElement({id: 'normal'});
@@ -191,7 +198,7 @@
           bot.userAgent.isEngineVersion(11)) {
         return;
       }
-    
+
       var el = document.getElementById('textarea');
       el.value = '';
       webdriver.atoms.element.type(el, ['hello\n\nworld']);
@@ -199,7 +206,7 @@
     }
   </script>
 </head>
-<body id="body" style="" name="body">
+<body id="body" class="c" style="" name="body">
 
 <p id="has_id" title="cheese">This has an ID</p>
 
diff --git a/javascript/webdriver/test/promise_test.html b/javascript/webdriver/test/promise_test.html
index ca3c21c..cbde295 100644
--- a/javascript/webdriver/test/promise_test.html
+++ b/javascript/webdriver/test/promise_test.html
@@ -856,7 +856,7 @@
       errback = callbackHelper(function(error) {
         assertEquals(e, error);
       }));
-
+  
   callback.assertNotCalled();
   errback.assertCalled();
 }
diff --git a/javascript/webdriver/test/stacktrace_test.html b/javascript/webdriver/test/stacktrace_test.html
index dbe60fb..f6446b4 100644
--- a/javascript/webdriver/test/stacktrace_test.html
+++ b/javascript/webdriver/test/stacktrace_test.html
@@ -18,6 +18,8 @@
 <title>stacktrace_test</title>
 <script src="test_bootstrap.js"></script>
 <script>
+  goog.require('bot.Error');
+  goog.require('bot.ErrorCode');
   goog.require('goog.string');
   goog.require('goog.testing.ExpectedFailures');
   goog.require('goog.testing.JsUnitException');
@@ -395,5 +397,19 @@
     '    at file:///bar/bar.js:1'
   ].join('\n'), ret.stack);
 }
+
+function testFormattingBotErrors() {
+  var error = new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT, 'boom');
+  var expectedStack = [
+    'NoSuchElementError: boom',
+    '    at Color.red (http://x:1234)',
+    '    at Foo.bar (http://y:5678)'
+  ].join('\n');
+  error.stack = expectedStack;
+
+  var ret = webdriver.stacktrace.format(error);
+  assertEquals(ret, error);
+  assertEquals(expectedStack, error.stack);
+}
 </script>
 
diff --git a/javascript/webdriver/test_e2e/actionsequece_test.html b/javascript/webdriver/test_e2e/actionsequence_test.html
similarity index 79%
rename from javascript/webdriver/test_e2e/actionsequece_test.html
rename to javascript/webdriver/test_e2e/actionsequence_test.html
index 784b82c..5a549b5 100644
--- a/javascript/webdriver/test_e2e/actionsequece_test.html
+++ b/javascript/webdriver/test_e2e/actionsequence_test.html
@@ -283,77 +283,6 @@
     });
   }
 
-  function assertListOrder(parent, var_args) {
-    var order = goog.array.slice(arguments, 1);
-    var divs = parent.querySelectorAll('div');
-    assertEquals('wrong # children', order.length, divs.length);
-
-    var expectedOrder = goog.array.map(order, function(item) {
-      return item + '';
-    });
-    var actualOrder = goog.array.map(divs, function(div) {
-      return div.innerHTML;
-    });
-    assertArrayEquals(expectedOrder, actualOrder);
-  }
-
-  function testDragAndDrop() {
-    assertListOrder(dom.draglist, 1, 2, 3, 4, 5, 6);
-
-    wd.draglist.findElements(By.tagName('div')).then(function(items) {
-      driver.actions().
-          dragAndDrop(items[4], items[1]).
-          dragAndDrop(items[3], items[4]).
-          dragAndDrop(items[5], items[4]).
-          perform();
-    });
-    driver.call(function() {
-      // Well, this is unfortunate. Firefox and Chrome compute coordinates
-      // differently, so we end up with different list orders.
-      if (goog.userAgent.GECKO) {
-        assertListOrder(dom.draglist, 4, 6, 1, 2, 5, 3);
-      } else {
-        assertListOrder(dom.draglist, 1, 4, 6, 5, 2, 3);
-      }
-    });
-  }
-
-  function testDragAndDropWithShift() {
-    assertListOrder(dom.dragshiftlist, 1, 2, 3, 4, 5, 6);
-
-    wd.dragshiftlist.findElements(By.tagName('div')).then(function(items) {
-      driver.actions().
-          dragAndDrop(items[4], items[1]).
-          dragAndDrop(items[3], items[4]).
-          dragAndDrop(items[5], items[4]).
-          perform();
-      driver.call(function() {
-        assertListOrder(dom.dragshiftlist, 1, 2, 3, 4, 5, 6);
-      });
-
-      driver.actions().
-          keyDown(webdriver.Key.SHIFT).
-          dragAndDrop(items[4], items[1]).
-          dragAndDrop(items[3], items[4]).
-          dragAndDrop(items[5], items[4]).
-          keyUp(webdriver.Key.SHIFT).
-          perform();
-      driver.call(function() {
-        // Well, this is unfortunate. Firefox and Chrome compute coordinates
-        // differently, so we end up with different list orders.
-        if (goog.userAgent.GECKO) {
-          // TODO(jleyba): Firefox does not execute shift+click correctly.
-          // Using a dummy assertion here until the bug is fixed.
-          // See http://code.google.com/p/selenium/issues/detail?id=3734
-          assertListOrder(dom.dragshiftlist, 1, 2, 3, 4, 5, 6);
-          // assertListOrder(dom.dragshiftlist, 4, 6, 1, 2, 5, 3);
-        } else {
-          assertListOrder(dom.dragshiftlist, 1, 4, 6, 5, 2, 3);
-        }
-      });
-    });
-  }
-
   function testMultiSelect() {
     assertEquals(-1, dom.select.selectedIndex);
 
diff --git a/rake-tasks/crazy_fun/mappings/javascript.rb b/rake-tasks/crazy_fun/mappings/javascript.rb
index 0ca71d5..0384f60 100644
--- a/rake-tasks/crazy_fun/mappings/javascript.rb
+++ b/rake-tasks/crazy_fun/mappings/javascript.rb
@@ -167,7 +167,7 @@
 module Javascript
   # CrazyFunJava.ant.taskdef :name      => "jscomp",
   #                          :classname => "com.google.javascript.jscomp.ant.CompileTask",
-  #                          :classpath => "third_party/closure/bin/compiler-20120710.jar"
+  #                          :classpath => "third_party/closure/bin/compiler-20120917.jar"
 
   class BaseJs < Tasks
     attr_reader :calcdeps
@@ -178,7 +178,7 @@
         py = "python"
       end
       @calcdeps = "#{py} third_party/closure/bin/calcdeps.py " +
-                  "-c third_party/closure/bin/compiler-20120710.jar "
+                  "-c third_party/closure/bin/compiler-20120917.jar "
     end
 
     def js_name(dir, name)
@@ -471,7 +471,7 @@
 
         CrazyFunJava.ant.java :classname => "com.google.javascript.jscomp.CommandLineRunner", :failonerror => true do
           classpath do
-            pathelement :path =>  "third_party/closure/bin/compiler-20120710.jar"
+            pathelement :path =>  "third_party/closure/bin/compiler-20120917.jar"
           end
           arg :line => cmd
         end
@@ -563,7 +563,7 @@
 
         CrazyFunJava.ant.java :classname => "com.google.javascript.jscomp.CommandLineRunner", :fork => false, :failonerror => true do
           classpath do
-            pathelement :path =>  "third_party/closure/bin/compiler-20120710.jar"
+            pathelement :path =>  "third_party/closure/bin/compiler-20120917.jar"
           end
           arg :line => cmd
         end
diff --git a/third_party/closure/bin/README b/third_party/closure/bin/README
index e6d12c4..270cfc2 100644
--- a/third_party/closure/bin/README
+++ b/third_party/closure/bin/README
@@ -146,7 +146,7 @@
 significantly for use by Google's JavaScript compiler.
 
 Local Modifications: The packages have been renamespaced. All code not
-relavant to parsing has been removed. A JSDoc parser and static typing
+relevant to parsing has been removed. A JsDoc parser and static typing
 system have been added.
 
 
@@ -171,7 +171,7 @@
 
 Args4j
 URL: https://args4j.dev.java.net/
-Version: 2.0.12
+Version: 2.0.16
 License: MIT
 
 Description:
@@ -187,7 +187,7 @@
 
 Guava Libraries
 URL: http://code.google.com/p/guava-libraries/
-Version:  r08
+Version:  13.0.1
 License: Apache License 2.0
 
 Description: Google's core Java libraries.
@@ -230,7 +230,7 @@
 
 JUnit
 URL:  http://sourceforge.net/projects/junit/
-Version:  4.8.2
+Version:  4.10
 License:  Common Public License 1.0
 
 Description: A framework for writing and running automated tests in Java.
@@ -244,7 +244,7 @@
 
 Protocol Buffers
 URL: http://code.google.com/p/protobuf/
-Version: 2.3.0
+Version: 2.4.1
 License: New BSD License
 
 Description: Supporting libraries for protocol buffers,
@@ -281,9 +281,9 @@
 
 ---
 Code in:
-tools/maven-ant-tasks-2.1.1.jar
+tools/maven-ant-tasks-2.1.3.jar
 URL: http://maven.apache.org
-Version 2.1.1
+Version 2.1.3
 License: Apache License 2.0
 Description:
   Maven Ant tasks are used to manage dependencies and to install/deploy to
diff --git a/third_party/closure/bin/compiler-20120710.jar b/third_party/closure/bin/compiler-20120710.jar
deleted file mode 100644
index 52a920f..0000000
--- a/third_party/closure/bin/compiler-20120710.jar
+++ /dev/null
Binary files differ
diff --git a/third_party/closure/bin/compiler-20120917.jar b/third_party/closure/bin/compiler-20120917.jar
new file mode 100644
index 0000000..e23352f
--- /dev/null
+++ b/third_party/closure/bin/compiler-20120917.jar
Binary files differ
diff --git a/third_party/closure/goog/base.js b/third_party/closure/goog/base.js
index ebf8870..99e44c5 100644
--- a/third_party/closure/goog/base.js
+++ b/third_party/closure/goog/base.js
@@ -18,6 +18,9 @@
  * In uncompiled mode base.js will write out Closure's deps file, unless the
  * global <code>CLOSURE_NO_DEPS</code> is set to true.  This allows projects to
  * include their own deps file(s) from different locations.
+ *
+ *
+ * @provideGoog
  */
 
 
@@ -537,6 +540,23 @@
   goog.writeScriptTag_ = function(src) {
     if (goog.inHtmlDocument_()) {
       var doc = goog.global.document;
+
+      // If the user tries to require a new symbol after document load,
+      // something has gone terribly wrong. Doing a document.write would
+      // wipe out the page.
+      if (doc.readyState == 'complete') {
+        // Certain test frameworks load base.js multiple times, which tries
+        // to write deps.js each time. If that happens, just fail silently.
+        // These frameworks wipe the page between each load of base.js, so this
+        // is OK.
+        var isDeps = /\bdeps.js$/.test(src);
+        if (isDeps) {
+          return false;
+        } else {
+          throw Error('Cannot write "' + src + '" after document load');
+        }
+      }
+
       doc.write(
           '<script type="text/javascript" src="' + src + '"></' + 'script>');
       return true;
@@ -1313,7 +1333,17 @@
 
 
 /**
- * Abstract implementation of goog.getMsg for use with localized messages.
+ * Gets a localized message.
+ *
+ * This function is a compiler primitive. If you give the compiler a localized
+ * message bundle, it will replace the string at compile-time with a localized
+ * version, and expand goog.getMsg call to a concatenated string.
+ *
+ * Messages must be initialized in the form:
+ * <code>
+ * var MSG_NAME = goog.getMsg('Hello {$placeholder}', {'placeholder': 'world'});
+ * </code>
+ *
  * @param {string} str Translatable string, places holders in the form {$foo}.
  * @param {Object=} opt_values Map of place holder name to value.
  * @return {string} message with placeholders filled.
@@ -1329,6 +1359,26 @@
 
 
 /**
+ * Gets a localized message. If the message does not have a translation, gives a
+ * fallback message.
+ *
+ * This is useful when introducing a new message that has not yet been
+ * translated into all languages.
+ *
+ * This function is a compiler primtive. Must be used in the form:
+ * <code>var x = goog.getMsgWithFallback(MSG_A, MSG_B);</code>
+ * where MSG_A and MSG_B were initialized with goog.getMsg.
+ *
+ * @param {string} a The preferred message.
+ * @param {string} b The fallback message.
+ * @return {string} The best translated message.
+ */
+goog.getMsgWithFallback = function(a, b) {
+  return a;
+};
+
+
+/**
  * Exposes an unobfuscated global namespace path for the given object.
  * Note that fields of the exported object *will* be obfuscated,
  * unless they are exported in turn via this function or
diff --git a/third_party/closure/goog/crypt/aes.js b/third_party/closure/goog/crypt/aes.js
new file mode 100644
index 0000000..9dad4f2
--- /dev/null
+++ b/third_party/closure/goog/crypt/aes.js
@@ -0,0 +1,1027 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Implementation of AES in JavaScript.
+ * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
+ *
+ */
+
+goog.provide('goog.crypt.Aes');
+
+goog.require('goog.asserts');
+goog.require('goog.crypt.BlockCipher');
+
+
+
+/**
+ * Implementation of AES in JavaScript.
+ * See http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
+ *
+ * WARNING: This is ECB mode only. If you are encrypting something
+ * longer than 16 bytes, or encrypting more than one value with the same key
+ * (so basically, always) you need to use this with a block cipher mode of
+ * operation.  See goog.crypt.Cbc.
+ *
+ * See http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation for more
+ * information.
+ *
+ * @constructor
+ * @implements {goog.crypt.BlockCipher}
+ * @param {!Array.<number>} key The key as an array of integers in {0, 255}.
+ *     The key must have lengths of 16, 24, or 32 integers for 128-,
+ *     192-, or 256-bit encryption, respectively.
+ */
+goog.crypt.Aes = function(key) {
+  goog.crypt.Aes.assertKeyArray_(key);
+
+  /**
+   * The AES key.
+   * @type {!Array.<number>}
+   * @private
+   */
+  this.key_ = key;
+
+  /**
+   * Key length, in words.
+   * @type {number}
+   * @private
+   */
+  this.keyLength_ = this.key_.length / 4;
+
+  /**
+   * Number of rounds.  Based on key length per AES spec.
+   * @type {number}
+   * @private
+   */
+  this.numberOfRounds_ = this.keyLength_ + 6;
+
+  /**
+   * 4x4 byte array containing the current state.
+   * @type {!Array.<Array.<number>>}
+   * @private
+   */
+  this.state_ = [[], [], [], []];
+
+  /**
+   * Scratch temporary array for calculation.
+   * @type {!Array.<Array.<number>>}
+   * @private
+   */
+  this.temp_ = [[], [], [], []];
+
+  this.keyExpansion_();
+};
+
+
+/**
+ * @define {boolean} Whether to call test method stubs.  This can be enabled
+ *     for unit testing.
+ */
+goog.crypt.Aes.ENABLE_TEST_MODE = false;
+
+
+/**
+ * @override
+ */
+goog.crypt.Aes.prototype.encrypt = function(input) {
+
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testKeySchedule_(0, this.keySchedule_, 0);
+  }
+
+  this.copyInput_(input);
+  this.addRoundKey_(0);
+
+  for (var round = 1; round < this.numberOfRounds_; ++round) {
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testKeySchedule_(round, this.keySchedule_, round);
+      this.testStartRound_(round, this.state_);
+    }
+
+    this.subBytes_(goog.crypt.Aes.SBOX_);
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterSubBytes_(round, this.state_);
+    }
+
+    this.shiftRows_();
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterShiftRows_(round, this.state_);
+    }
+
+    this.mixColumns_();
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterMixColumns_(round, this.state_);
+    }
+
+    this.addRoundKey_(round);
+  }
+
+  this.subBytes_(goog.crypt.Aes.SBOX_);
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testAfterSubBytes_(round, this.state_);
+  }
+
+  this.shiftRows_();
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testAfterShiftRows_(round, this.state_);
+  }
+
+  this.addRoundKey_(this.numberOfRounds_);
+
+  return this.generateOutput_();
+};
+
+
+/**
+ * @override
+ */
+goog.crypt.Aes.prototype.decrypt = function(input) {
+
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testKeySchedule_(0, this.keySchedule_, this.numberOfRounds_);
+  }
+
+  this.copyInput_(input);
+  this.addRoundKey_(this.numberOfRounds_);
+
+  for (var round = 1; round < this.numberOfRounds_; ++round) {
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testKeySchedule_(round, this.keySchedule_,
+                            this.numberOfRounds_ - round);
+      this.testStartRound_(round, this.state_);
+    }
+
+    this.invShiftRows_();
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterShiftRows_(round, this.state_);
+    }
+
+    this.subBytes_(goog.crypt.Aes.INV_SBOX_);
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterSubBytes_(round, this.state_);
+    }
+
+    this.addRoundKey_(this.numberOfRounds_ - round);
+    if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+      this.testAfterAddRoundKey_(round, this.state_);
+    }
+
+    this.invMixColumns_();
+  }
+
+  this.invShiftRows_();
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testAfterShiftRows_(round, this.state_);
+  }
+
+  this.subBytes_(goog.crypt.Aes.INV_SBOX_);
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testAfterSubBytes_(this.numberOfRounds_, this.state_);
+  }
+
+  if (goog.crypt.Aes.ENABLE_TEST_MODE) {
+    this.testKeySchedule_(this.numberOfRounds_, this.keySchedule_, 0);
+  }
+
+  this.addRoundKey_(0);
+
+  return this.generateOutput_();
+};
+
+
+/**
+ * Block size, in words.  Fixed at 4 per AES spec.
+ * @type {number}
+ * @private
+ */
+goog.crypt.Aes.BLOCK_SIZE_ = 4;
+
+
+/**
+ * Asserts that the key's array of integers is in the correct format.
+ * @param {!Array.<number>} arr AES key as array of integers.
+ * @private
+ */
+goog.crypt.Aes.assertKeyArray_ = function(arr) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    goog.asserts.assert(arr.length == 16 || arr.length == 24 ||
+                        arr.length == 32,
+                        'Key must have length 16, 24, or 32.');
+    for (var i = 0; i < arr.length; i++) {
+      goog.asserts.assertNumber(arr[i]);
+      goog.asserts.assert(arr[i] >= 0 && arr[i] <= 255);
+    }
+  }
+};
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * at the start of each round *in both functions encrypt() and decrypt()*.
+ * @param {number} roundNum Round number.
+ * @param {!Array.<number>} Current state.
+ * @private
+ */
+goog.crypt.Aes.prototype.testStartRound_ = goog.nullFunction;
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * each round right after the SubBytes step gets executed *in both functions
+ * encrypt() and decrypt()*.
+ * @param {number} roundNum Round number.
+ * @param {!Array.<number>} Current state.
+ * @private
+ */
+goog.crypt.Aes.prototype.testAfterSubBytes_ = goog.nullFunction;
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * each round right after the ShiftRows step gets executed *in both functions
+ * encrypt() and decrypt()*.
+ * @param {number} roundNum Round number.
+ * @param {!Array.<number>} Current state.
+ * @private
+ */
+goog.crypt.Aes.prototype.testAfterShiftRows_ = goog.nullFunction;
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * each round right after the MixColumns step gets executed *but only in the
+ * decrypt() function*.
+ * @param {number} roundNum Round number.
+ * @param {!Array.<number>} Current state.
+ * @private
+ */
+goog.crypt.Aes.prototype.testAfterMixColumns_ = goog.nullFunction;
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * each round right after the AddRoundKey step gets executed  encrypt().
+ * @param {number} roundNum Round number.
+ * @param {!Array.<number>} Current state.
+ * @private
+ */
+goog.crypt.Aes.prototype.testAfterAddRoundKey_ = goog.nullFunction;
+
+
+/**
+ * Tests can populate this with a callback, and that callback will get called
+ * before each round on the round key.  *Gets called in both the encrypt() and
+ * decrypt() functions.*
+ * @param {number} roundNum Round number.
+ * @param {!Array.<Array.<number>>} Computed key schedule.
+ * @param {number} index The index into the key schedule to test. This is not
+ *     necessarily roundNum because the key schedule is used in reverse
+ *     in the case of decryption.
+ * @private
+ */
+goog.crypt.Aes.prototype.testKeySchedule_ = goog.nullFunction;
+
+
+/**
+ * Helper to copy input into the AES state matrix.
+ * @param {!Array.<number>} input Byte array to copy into the state matrix.
+ * @private
+ */
+goog.crypt.Aes.prototype.copyInput_ = function(input) {
+  var v, p;
+
+  goog.asserts.assert(input.length == goog.crypt.Aes.BLOCK_SIZE_ * 4,
+                      'Expecting input of 4 times block size.');
+
+  for (var r = 0; r < goog.crypt.Aes.BLOCK_SIZE_; r++) {
+    for (var c = 0; c < 4; c++) {
+      p = c * 4 + r;
+      v = input[p];
+
+      goog.asserts.assert(
+          v <= 255 && v >= 0,
+          'Invalid input. Value %s at position %s is not a byte.', v, p);
+
+      this.state_[r][c] = v;
+    }
+  }
+};
+
+
+/**
+ * Helper to copy the state matrix into an output array.
+ * @return {!Array.<number>} Output byte array.
+ * @private
+ */
+goog.crypt.Aes.prototype.generateOutput_ = function() {
+  var output = [];
+  for (var r = 0; r < goog.crypt.Aes.BLOCK_SIZE_; r++) {
+    for (var c = 0; c < 4; c++) {
+      output[c * 4 + r] = this.state_[r][c];
+    }
+  }
+  return output;
+};
+
+
+/**
+ * AES's AddRoundKey procedure. Add the current round key to the state.
+ * @param {number} round The current round.
+ * @private
+ */
+goog.crypt.Aes.prototype.addRoundKey_ = function(round) {
+  for (var r = 0; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.state_[r][c] ^= this.keySchedule_[round * 4 + c][r];
+    }
+  }
+};
+
+
+/**
+ * AES's SubBytes procedure. Substitute bytes from the precomputed SBox lookup
+ * into the state.
+ * @param {!Array.<number>} box The SBox or invSBox.
+ * @private
+ */
+goog.crypt.Aes.prototype.subBytes_ = function(box) {
+  for (var r = 0; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.state_[r][c] = box[this.state_[r][c]];
+    }
+  }
+};
+
+
+/**
+ * AES's ShiftRows procedure. Shift the values in each row to the right. Each
+ * row is shifted one more slot than the one above it.
+ * @private
+ */
+goog.crypt.Aes.prototype.shiftRows_ = function() {
+  for (var r = 1; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.temp_[r][c] = this.state_[r][c];
+    }
+  }
+
+  for (var r = 1; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.state_[r][c] = this.temp_[r][(c + r) %
+          goog.crypt.Aes.BLOCK_SIZE_];
+    }
+  }
+};
+
+
+/**
+ * AES's InvShiftRows procedure. Shift the values in each row to the right.
+ * @private
+ */
+goog.crypt.Aes.prototype.invShiftRows_ = function() {
+  for (var r = 1; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.temp_[r][(c + r) % goog.crypt.Aes.BLOCK_SIZE_] =
+          this.state_[r][c];
+    }
+  }
+
+  for (var r = 1; r < 4; r++) {
+    for (var c = 0; c < 4; c++) {
+      this.state_[r][c] = this.temp_[r][c];
+    }
+  }
+};
+
+
+/**
+ * AES's MixColumns procedure. Mix the columns of the state using magic.
+ * @private
+ */
+goog.crypt.Aes.prototype.mixColumns_ = function() {
+  var s = this.state_;
+  var t = this.temp_[0];
+
+  for (var c = 0; c < 4; c++) {
+    t[0] = s[0][c];
+    t[1] = s[1][c];
+    t[2] = s[2][c];
+    t[3] = s[3][c];
+
+    s[0][c] = (goog.crypt.Aes.MULT_2_[t[0]] ^
+               goog.crypt.Aes.MULT_3_[t[1]] ^ t[2] ^ t[3]);
+    s[1][c] = (t[0] ^ goog.crypt.Aes.MULT_2_[t[1]] ^
+               goog.crypt.Aes.MULT_3_[t[2]] ^ t[3]);
+    s[2][c] = (t[0] ^ t[1] ^ goog.crypt.Aes.MULT_2_[t[2]] ^
+               goog.crypt.Aes.MULT_3_[t[3]]);
+    s[3][c] = (goog.crypt.Aes.MULT_3_[t[0]] ^ t[1] ^ t[2] ^
+               goog.crypt.Aes.MULT_2_[t[3]]);
+  }
+};
+
+
+/**
+ * AES's InvMixColumns procedure.
+ * @private
+ */
+goog.crypt.Aes.prototype.invMixColumns_ = function() {
+  var s = this.state_;
+  var t = this.temp_[0];
+
+  for (var c = 0; c < 4; c++) {
+    t[0] = s[0][c];
+    t[1] = s[1][c];
+    t[2] = s[2][c];
+    t[3] = s[3][c];
+
+    s[0][c] = (
+        goog.crypt.Aes.MULT_E_[t[0]] ^ goog.crypt.Aes.MULT_B_[t[1]] ^
+        goog.crypt.Aes.MULT_D_[t[2]] ^ goog.crypt.Aes.MULT_9_[t[3]]);
+
+    s[1][c] = (
+        goog.crypt.Aes.MULT_9_[t[0]] ^ goog.crypt.Aes.MULT_E_[t[1]] ^
+        goog.crypt.Aes.MULT_B_[t[2]] ^ goog.crypt.Aes.MULT_D_[t[3]]);
+
+    s[2][c] = (
+        goog.crypt.Aes.MULT_D_[t[0]] ^ goog.crypt.Aes.MULT_9_[t[1]] ^
+        goog.crypt.Aes.MULT_E_[t[2]] ^ goog.crypt.Aes.MULT_B_[t[3]]);
+
+    s[3][c] = (
+        goog.crypt.Aes.MULT_B_[t[0]] ^ goog.crypt.Aes.MULT_D_[t[1]] ^
+        goog.crypt.Aes.MULT_9_[t[2]] ^ goog.crypt.Aes.MULT_E_[t[3]]);
+  }
+};
+
+
+/**
+ * AES's KeyExpansion procedure. Create the key schedule from the initial key.
+ * @private
+ */
+goog.crypt.Aes.prototype.keyExpansion_ = function() {
+  this.keySchedule_ = new Array(goog.crypt.Aes.BLOCK_SIZE_ * (
+      this.numberOfRounds_ + 1));
+
+  for (var rowNum = 0; rowNum < this.keyLength_; rowNum++) {
+    this.keySchedule_[rowNum] = [
+      this.key_[4 * rowNum],
+      this.key_[4 * rowNum + 1],
+      this.key_[4 * rowNum + 2],
+      this.key_[4 * rowNum + 3]
+    ];
+  }
+
+  var temp = new Array(4);
+
+  for (var rowNum = this.keyLength_;
+       rowNum < (goog.crypt.Aes.BLOCK_SIZE_ * (this.numberOfRounds_ + 1));
+       rowNum++) {
+    temp[0] = this.keySchedule_[rowNum - 1][0];
+    temp[1] = this.keySchedule_[rowNum - 1][1];
+    temp[2] = this.keySchedule_[rowNum - 1][2];
+    temp[3] = this.keySchedule_[rowNum - 1][3];
+
+    if (rowNum % this.keyLength_ == 0) {
+      this.rotWord_(temp);
+      this.subWord_(temp);
+
+      temp[0] ^= goog.crypt.Aes.RCON_[rowNum / this.keyLength_][0];
+      temp[1] ^= goog.crypt.Aes.RCON_[rowNum / this.keyLength_][1];
+      temp[2] ^= goog.crypt.Aes.RCON_[rowNum / this.keyLength_][2];
+      temp[3] ^= goog.crypt.Aes.RCON_[rowNum / this.keyLength_][3];
+    } else if (this.keyLength_ > 6 && rowNum % this.keyLength_ == 4) {
+      this.subWord_(temp);
+    }
+
+    this.keySchedule_[rowNum] = new Array(4);
+    this.keySchedule_[rowNum][0] =
+        this.keySchedule_[rowNum - this.keyLength_][0] ^ temp[0];
+    this.keySchedule_[rowNum][1] =
+        this.keySchedule_[rowNum - this.keyLength_][1] ^ temp[1];
+    this.keySchedule_[rowNum][2] =
+        this.keySchedule_[rowNum - this.keyLength_][2] ^ temp[2];
+    this.keySchedule_[rowNum][3] =
+        this.keySchedule_[rowNum - this.keyLength_][3] ^ temp[3];
+  }
+};
+
+
+/**
+ * AES's SubWord procedure.
+ * @param {!Array.<number>} w Bytes to find the SBox substitution for.
+ * @return {!Array.<number>} The substituted bytes.
+ * @private
+ */
+goog.crypt.Aes.prototype.subWord_ = function(w) {
+  w[0] = goog.crypt.Aes.SBOX_[w[0]];
+  w[1] = goog.crypt.Aes.SBOX_[w[1]];
+  w[2] = goog.crypt.Aes.SBOX_[w[2]];
+  w[3] = goog.crypt.Aes.SBOX_[w[3]];
+
+  return w;
+};
+
+
+/**
+ * AES's RotWord procedure.
+ * @param {!Array.<number>} w Array of bytes to rotate.
+ * @return {!Array.<number>} The rotated bytes.
+ * @private
+ */
+goog.crypt.Aes.prototype.rotWord_ = function(w) {
+  var temp = w[0];
+
+  w[0] = w[1];
+  w[1] = w[2];
+  w[2] = w[3];
+  w[3] = temp;
+
+  return w;
+};
+
+
+/**
+ * The key schedule.
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.prototype.keySchedule_;
+
+
+/**
+ * Precomputed SBox lookup.
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.SBOX_ = [
+  0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe,
+  0xd7, 0xab, 0x76,
+
+  0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c,
+  0xa4, 0x72, 0xc0,
+
+  0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71,
+  0xd8, 0x31, 0x15,
+
+  0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb,
+  0x27, 0xb2, 0x75,
+
+  0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29,
+  0xe3, 0x2f, 0x84,
+
+  0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a,
+  0x4c, 0x58, 0xcf,
+
+  0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50,
+  0x3c, 0x9f, 0xa8,
+
+  0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10,
+  0xff, 0xf3, 0xd2,
+
+  0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64,
+  0x5d, 0x19, 0x73,
+
+  0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde,
+  0x5e, 0x0b, 0xdb,
+
+  0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91,
+  0x95, 0xe4, 0x79,
+
+  0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65,
+  0x7a, 0xae, 0x08,
+
+  0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b,
+  0xbd, 0x8b, 0x8a,
+
+  0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86,
+  0xc1, 0x1d, 0x9e,
+
+  0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce,
+  0x55, 0x28, 0xdf,
+
+  0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0,
+  0x54, 0xbb, 0x16
+];
+
+
+/**
+ * Precomputed InvSBox lookup.
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.INV_SBOX_ = [
+  0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81,
+  0xf3, 0xd7, 0xfb,
+
+  0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4,
+  0xde, 0xe9, 0xcb,
+
+  0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42,
+  0xfa, 0xc3, 0x4e,
+
+  0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d,
+  0x8b, 0xd1, 0x25,
+
+  0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d,
+  0x65, 0xb6, 0x92,
+
+  0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7,
+  0x8d, 0x9d, 0x84,
+
+  0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8,
+  0xb3, 0x45, 0x06,
+
+  0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01,
+  0x13, 0x8a, 0x6b,
+
+  0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0,
+  0xb4, 0xe6, 0x73,
+
+  0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c,
+  0x75, 0xdf, 0x6e,
+
+  0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa,
+  0x18, 0xbe, 0x1b,
+
+  0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78,
+  0xcd, 0x5a, 0xf4,
+
+  0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27,
+  0x80, 0xec, 0x5f,
+
+  0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93,
+  0xc9, 0x9c, 0xef,
+
+  0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83,
+  0x53, 0x99, 0x61,
+
+  0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55,
+  0x21, 0x0c, 0x7d
+];
+
+
+/**
+ * Precomputed RCon lookup.
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.RCON_ = [
+  [0x00, 0x00, 0x00, 0x00],
+  [0x01, 0x00, 0x00, 0x00],
+  [0x02, 0x00, 0x00, 0x00],
+  [0x04, 0x00, 0x00, 0x00],
+  [0x08, 0x00, 0x00, 0x00],
+  [0x10, 0x00, 0x00, 0x00],
+  [0x20, 0x00, 0x00, 0x00],
+  [0x40, 0x00, 0x00, 0x00],
+  [0x80, 0x00, 0x00, 0x00],
+  [0x1b, 0x00, 0x00, 0x00],
+  [0x36, 0x00, 0x00, 0x00]
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 2 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_2_ = [
+  0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16,
+  0x18, 0x1A, 0x1C, 0x1E,
+
+  0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36,
+  0x38, 0x3A, 0x3C, 0x3E,
+
+  0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, 0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56,
+  0x58, 0x5A, 0x5C, 0x5E,
+
+  0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70, 0x72, 0x74, 0x76,
+  0x78, 0x7A, 0x7C, 0x7E,
+
+  0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96,
+  0x98, 0x9A, 0x9C, 0x9E,
+
+  0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6,
+  0xB8, 0xBA, 0xBC, 0xBE,
+
+  0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6,
+  0xD8, 0xDA, 0xDC, 0xDE,
+
+  0xE0, 0xE2, 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6,
+  0xF8, 0xFA, 0xFC, 0xFE,
+
+  0x1B, 0x19, 0x1F, 0x1D, 0x13, 0x11, 0x17, 0x15, 0x0B, 0x09, 0x0F, 0x0D,
+  0x03, 0x01, 0x07, 0x05,
+
+  0x3B, 0x39, 0x3F, 0x3D, 0x33, 0x31, 0x37, 0x35, 0x2B, 0x29, 0x2F, 0x2D,
+  0x23, 0x21, 0x27, 0x25,
+
+  0x5B, 0x59, 0x5F, 0x5D, 0x53, 0x51, 0x57, 0x55, 0x4B, 0x49, 0x4F, 0x4D,
+  0x43, 0x41, 0x47, 0x45,
+
+  0x7B, 0x79, 0x7F, 0x7D, 0x73, 0x71, 0x77, 0x75, 0x6B, 0x69, 0x6F, 0x6D,
+  0x63, 0x61, 0x67, 0x65,
+
+  0x9B, 0x99, 0x9F, 0x9D, 0x93, 0x91, 0x97, 0x95, 0x8B, 0x89, 0x8F, 0x8D,
+  0x83, 0x81, 0x87, 0x85,
+
+  0xBB, 0xB9, 0xBF, 0xBD, 0xB3, 0xB1, 0xB7, 0xB5, 0xAB, 0xA9, 0xAF, 0xAD,
+  0xA3, 0xA1, 0xA7, 0xA5,
+
+  0xDB, 0xD9, 0xDF, 0xDD, 0xD3, 0xD1, 0xD7, 0xD5, 0xCB, 0xC9, 0xCF, 0xCD,
+  0xC3, 0xC1, 0xC7, 0xC5,
+
+  0xFB, 0xF9, 0xFF, 0xFD, 0xF3, 0xF1, 0xF7, 0xF5, 0xEB, 0xE9, 0xEF, 0xED,
+  0xE3, 0xE1, 0xE7, 0xE5
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 3 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_3_ = [
+  0x00, 0x03, 0x06, 0x05, 0x0C, 0x0F, 0x0A, 0x09, 0x18, 0x1B, 0x1E, 0x1D,
+  0x14, 0x17, 0x12, 0x11,
+
+  0x30, 0x33, 0x36, 0x35, 0x3C, 0x3F, 0x3A, 0x39, 0x28, 0x2B, 0x2E, 0x2D,
+  0x24, 0x27, 0x22, 0x21,
+
+  0x60, 0x63, 0x66, 0x65, 0x6C, 0x6F, 0x6A, 0x69, 0x78, 0x7B, 0x7E, 0x7D,
+  0x74, 0x77, 0x72, 0x71,
+
+  0x50, 0x53, 0x56, 0x55, 0x5C, 0x5F, 0x5A, 0x59, 0x48, 0x4B, 0x4E, 0x4D,
+  0x44, 0x47, 0x42, 0x41,
+
+  0xC0, 0xC3, 0xC6, 0xC5, 0xCC, 0xCF, 0xCA, 0xC9, 0xD8, 0xDB, 0xDE, 0xDD,
+  0xD4, 0xD7, 0xD2, 0xD1,
+
+  0xF0, 0xF3, 0xF6, 0xF5, 0xFC, 0xFF, 0xFA, 0xF9, 0xE8, 0xEB, 0xEE, 0xED,
+  0xE4, 0xE7, 0xE2, 0xE1,
+
+  0xA0, 0xA3, 0xA6, 0xA5, 0xAC, 0xAF, 0xAA, 0xA9, 0xB8, 0xBB, 0xBE, 0xBD,
+  0xB4, 0xB7, 0xB2, 0xB1,
+
+  0x90, 0x93, 0x96, 0x95, 0x9C, 0x9F, 0x9A, 0x99, 0x88, 0x8B, 0x8E, 0x8D,
+  0x84, 0x87, 0x82, 0x81,
+
+  0x9B, 0x98, 0x9D, 0x9E, 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86,
+  0x8F, 0x8C, 0x89, 0x8A,
+
+  0xAB, 0xA8, 0xAD, 0xAE, 0xA7, 0xA4, 0xA1, 0xA2, 0xB3, 0xB0, 0xB5, 0xB6,
+  0xBF, 0xBC, 0xB9, 0xBA,
+
+  0xFB, 0xF8, 0xFD, 0xFE, 0xF7, 0xF4, 0xF1, 0xF2, 0xE3, 0xE0, 0xE5, 0xE6,
+  0xEF, 0xEC, 0xE9, 0xEA,
+
+  0xCB, 0xC8, 0xCD, 0xCE, 0xC7, 0xC4, 0xC1, 0xC2, 0xD3, 0xD0, 0xD5, 0xD6,
+  0xDF, 0xDC, 0xD9, 0xDA,
+
+  0x5B, 0x58, 0x5D, 0x5E, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46,
+  0x4F, 0x4C, 0x49, 0x4A,
+
+  0x6B, 0x68, 0x6D, 0x6E, 0x67, 0x64, 0x61, 0x62, 0x73, 0x70, 0x75, 0x76,
+  0x7F, 0x7C, 0x79, 0x7A,
+
+  0x3B, 0x38, 0x3D, 0x3E, 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26,
+  0x2F, 0x2C, 0x29, 0x2A,
+
+  0x0B, 0x08, 0x0D, 0x0E, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16,
+  0x1F, 0x1C, 0x19, 0x1A
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 9 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_9_ = [
+  0x00, 0x09, 0x12, 0x1B, 0x24, 0x2D, 0x36, 0x3F, 0x48, 0x41, 0x5A, 0x53,
+  0x6C, 0x65, 0x7E, 0x77,
+
+  0x90, 0x99, 0x82, 0x8B, 0xB4, 0xBD, 0xA6, 0xAF, 0xD8, 0xD1, 0xCA, 0xC3,
+  0xFC, 0xF5, 0xEE, 0xE7,
+
+  0x3B, 0x32, 0x29, 0x20, 0x1F, 0x16, 0x0D, 0x04, 0x73, 0x7A, 0x61, 0x68,
+  0x57, 0x5E, 0x45, 0x4C,
+
+  0xAB, 0xA2, 0xB9, 0xB0, 0x8F, 0x86, 0x9D, 0x94, 0xE3, 0xEA, 0xF1, 0xF8,
+  0xC7, 0xCE, 0xD5, 0xDC,
+
+  0x76, 0x7F, 0x64, 0x6D, 0x52, 0x5B, 0x40, 0x49, 0x3E, 0x37, 0x2C, 0x25,
+  0x1A, 0x13, 0x08, 0x01,
+
+  0xE6, 0xEF, 0xF4, 0xFD, 0xC2, 0xCB, 0xD0, 0xD9, 0xAE, 0xA7, 0xBC, 0xB5,
+  0x8A, 0x83, 0x98, 0x91,
+
+  0x4D, 0x44, 0x5F, 0x56, 0x69, 0x60, 0x7B, 0x72, 0x05, 0x0C, 0x17, 0x1E,
+  0x21, 0x28, 0x33, 0x3A,
+
+  0xDD, 0xD4, 0xCF, 0xC6, 0xF9, 0xF0, 0xEB, 0xE2, 0x95, 0x9C, 0x87, 0x8E,
+  0xB1, 0xB8, 0xA3, 0xAA,
+
+  0xEC, 0xE5, 0xFE, 0xF7, 0xC8, 0xC1, 0xDA, 0xD3, 0xA4, 0xAD, 0xB6, 0xBF,
+  0x80, 0x89, 0x92, 0x9B,
+
+  0x7C, 0x75, 0x6E, 0x67, 0x58, 0x51, 0x4A, 0x43, 0x34, 0x3D, 0x26, 0x2F,
+  0x10, 0x19, 0x02, 0x0B,
+
+  0xD7, 0xDE, 0xC5, 0xCC, 0xF3, 0xFA, 0xE1, 0xE8, 0x9F, 0x96, 0x8D, 0x84,
+  0xBB, 0xB2, 0xA9, 0xA0,
+
+  0x47, 0x4E, 0x55, 0x5C, 0x63, 0x6A, 0x71, 0x78, 0x0F, 0x06, 0x1D, 0x14,
+  0x2B, 0x22, 0x39, 0x30,
+
+  0x9A, 0x93, 0x88, 0x81, 0xBE, 0xB7, 0xAC, 0xA5, 0xD2, 0xDB, 0xC0, 0xC9,
+  0xF6, 0xFF, 0xE4, 0xED,
+
+  0x0A, 0x03, 0x18, 0x11, 0x2E, 0x27, 0x3C, 0x35, 0x42, 0x4B, 0x50, 0x59,
+  0x66, 0x6F, 0x74, 0x7D,
+
+  0xA1, 0xA8, 0xB3, 0xBA, 0x85, 0x8C, 0x97, 0x9E, 0xE9, 0xE0, 0xFB, 0xF2,
+  0xCD, 0xC4, 0xDF, 0xD6,
+
+  0x31, 0x38, 0x23, 0x2A, 0x15, 0x1C, 0x07, 0x0E, 0x79, 0x70, 0x6B, 0x62,
+  0x5D, 0x54, 0x4F, 0x46
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 11 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_B_ = [
+  0x00, 0x0B, 0x16, 0x1D, 0x2C, 0x27, 0x3A, 0x31, 0x58, 0x53, 0x4E, 0x45,
+  0x74, 0x7F, 0x62, 0x69,
+
+  0xB0, 0xBB, 0xA6, 0xAD, 0x9C, 0x97, 0x8A, 0x81, 0xE8, 0xE3, 0xFE, 0xF5,
+  0xC4, 0xCF, 0xD2, 0xD9,
+
+  0x7B, 0x70, 0x6D, 0x66, 0x57, 0x5C, 0x41, 0x4A, 0x23, 0x28, 0x35, 0x3E,
+  0x0F, 0x04, 0x19, 0x12,
+
+  0xCB, 0xC0, 0xDD, 0xD6, 0xE7, 0xEC, 0xF1, 0xFA, 0x93, 0x98, 0x85, 0x8E,
+  0xBF, 0xB4, 0xA9, 0xA2,
+
+  0xF6, 0xFD, 0xE0, 0xEB, 0xDA, 0xD1, 0xCC, 0xC7, 0xAE, 0xA5, 0xB8, 0xB3,
+  0x82, 0x89, 0x94, 0x9F,
+
+  0x46, 0x4D, 0x50, 0x5B, 0x6A, 0x61, 0x7C, 0x77, 0x1E, 0x15, 0x08, 0x03,
+  0x32, 0x39, 0x24, 0x2F,
+
+  0x8D, 0x86, 0x9B, 0x90, 0xA1, 0xAA, 0xB7, 0xBC, 0xD5, 0xDE, 0xC3, 0xC8,
+  0xF9, 0xF2, 0xEF, 0xE4,
+
+  0x3D, 0x36, 0x2B, 0x20, 0x11, 0x1A, 0x07, 0x0C, 0x65, 0x6E, 0x73, 0x78,
+  0x49, 0x42, 0x5F, 0x54,
+
+  0xF7, 0xFC, 0xE1, 0xEA, 0xDB, 0xD0, 0xCD, 0xC6, 0xAF, 0xA4, 0xB9, 0xB2,
+  0x83, 0x88, 0x95, 0x9E,
+
+  0x47, 0x4C, 0x51, 0x5A, 0x6B, 0x60, 0x7D, 0x76, 0x1F, 0x14, 0x09, 0x02,
+  0x33, 0x38, 0x25, 0x2E,
+
+  0x8C, 0x87, 0x9A, 0x91, 0xA0, 0xAB, 0xB6, 0xBD, 0xD4, 0xDF, 0xC2, 0xC9,
+  0xF8, 0xF3, 0xEE, 0xE5,
+
+  0x3C, 0x37, 0x2A, 0x21, 0x10, 0x1B, 0x06, 0x0D, 0x64, 0x6F, 0x72, 0x79,
+  0x48, 0x43, 0x5E, 0x55,
+
+  0x01, 0x0A, 0x17, 0x1C, 0x2D, 0x26, 0x3B, 0x30, 0x59, 0x52, 0x4F, 0x44,
+  0x75, 0x7E, 0x63, 0x68,
+
+  0xB1, 0xBA, 0xA7, 0xAC, 0x9D, 0x96, 0x8B, 0x80, 0xE9, 0xE2, 0xFF, 0xF4,
+  0xC5, 0xCE, 0xD3, 0xD8,
+
+  0x7A, 0x71, 0x6C, 0x67, 0x56, 0x5D, 0x40, 0x4B, 0x22, 0x29, 0x34, 0x3F,
+  0x0E, 0x05, 0x18, 0x13,
+
+  0xCA, 0xC1, 0xDC, 0xD7, 0xE6, 0xED, 0xF0, 0xFB, 0x92, 0x99, 0x84, 0x8F,
+  0xBE, 0xB5, 0xA8, 0xA3
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 13 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_D_ = [
+  0x00, 0x0D, 0x1A, 0x17, 0x34, 0x39, 0x2E, 0x23, 0x68, 0x65, 0x72, 0x7F,
+  0x5C, 0x51, 0x46, 0x4B,
+
+  0xD0, 0xDD, 0xCA, 0xC7, 0xE4, 0xE9, 0xFE, 0xF3, 0xB8, 0xB5, 0xA2, 0xAF,
+  0x8C, 0x81, 0x96, 0x9B,
+
+  0xBB, 0xB6, 0xA1, 0xAC, 0x8F, 0x82, 0x95, 0x98, 0xD3, 0xDE, 0xC9, 0xC4,
+  0xE7, 0xEA, 0xFD, 0xF0,
+
+  0x6B, 0x66, 0x71, 0x7C, 0x5F, 0x52, 0x45, 0x48, 0x03, 0x0E, 0x19, 0x14,
+  0x37, 0x3A, 0x2D, 0x20,
+
+  0x6D, 0x60, 0x77, 0x7A, 0x59, 0x54, 0x43, 0x4E, 0x05, 0x08, 0x1F, 0x12,
+  0x31, 0x3C, 0x2B, 0x26,
+
+  0xBD, 0xB0, 0xA7, 0xAA, 0x89, 0x84, 0x93, 0x9E, 0xD5, 0xD8, 0xCF, 0xC2,
+  0xE1, 0xEC, 0xFB, 0xF6,
+
+  0xD6, 0xDB, 0xCC, 0xC1, 0xE2, 0xEF, 0xF8, 0xF5, 0xBE, 0xB3, 0xA4, 0xA9,
+  0x8A, 0x87, 0x90, 0x9D,
+
+  0x06, 0x0B, 0x1C, 0x11, 0x32, 0x3F, 0x28, 0x25, 0x6E, 0x63, 0x74, 0x79,
+  0x5A, 0x57, 0x40, 0x4D,
+
+  0xDA, 0xD7, 0xC0, 0xCD, 0xEE, 0xE3, 0xF4, 0xF9, 0xB2, 0xBF, 0xA8, 0xA5,
+  0x86, 0x8B, 0x9C, 0x91,
+
+  0x0A, 0x07, 0x10, 0x1D, 0x3E, 0x33, 0x24, 0x29, 0x62, 0x6F, 0x78, 0x75,
+  0x56, 0x5B, 0x4C, 0x41,
+
+  0x61, 0x6C, 0x7B, 0x76, 0x55, 0x58, 0x4F, 0x42, 0x09, 0x04, 0x13, 0x1E,
+  0x3D, 0x30, 0x27, 0x2A,
+
+  0xB1, 0xBC, 0xAB, 0xA6, 0x85, 0x88, 0x9F, 0x92, 0xD9, 0xD4, 0xC3, 0xCE,
+  0xED, 0xE0, 0xF7, 0xFA,
+
+  0xB7, 0xBA, 0xAD, 0xA0, 0x83, 0x8E, 0x99, 0x94, 0xDF, 0xD2, 0xC5, 0xC8,
+  0xEB, 0xE6, 0xF1, 0xFC,
+
+  0x67, 0x6A, 0x7D, 0x70, 0x53, 0x5E, 0x49, 0x44, 0x0F, 0x02, 0x15, 0x18,
+  0x3B, 0x36, 0x21, 0x2C,
+
+  0x0C, 0x01, 0x16, 0x1B, 0x38, 0x35, 0x22, 0x2F, 0x64, 0x69, 0x7E, 0x73,
+  0x50, 0x5D, 0x4A, 0x47,
+
+  0xDC, 0xD1, 0xC6, 0xCB, 0xE8, 0xE5, 0xF2, 0xFF, 0xB4, 0xB9, 0xAE, 0xA3,
+  0x80, 0x8D, 0x9A, 0x97
+];
+
+
+/**
+ * Precomputed lookup of multiplication by 14 in GF(2^8)
+ * @type {!Array.<number>}
+ * @private
+ */
+goog.crypt.Aes.MULT_E_ = [
+  0x00, 0x0E, 0x1C, 0x12, 0x38, 0x36, 0x24, 0x2A, 0x70, 0x7E, 0x6C, 0x62,
+  0x48, 0x46, 0x54, 0x5A,
+
+  0xE0, 0xEE, 0xFC, 0xF2, 0xD8, 0xD6, 0xC4, 0xCA, 0x90, 0x9E, 0x8C, 0x82,
+  0xA8, 0xA6, 0xB4, 0xBA,
+
+  0xDB, 0xD5, 0xC7, 0xC9, 0xE3, 0xED, 0xFF, 0xF1, 0xAB, 0xA5, 0xB7, 0xB9,
+  0x93, 0x9D, 0x8F, 0x81,
+
+  0x3B, 0x35, 0x27, 0x29, 0x03, 0x0D, 0x1F, 0x11, 0x4B, 0x45, 0x57, 0x59,
+  0x73, 0x7D, 0x6F, 0x61,
+
+  0xAD, 0xA3, 0xB1, 0xBF, 0x95, 0x9B, 0x89, 0x87, 0xDD, 0xD3, 0xC1, 0xCF,
+  0xE5, 0xEB, 0xF9, 0xF7,
+
+  0x4D, 0x43, 0x51, 0x5F, 0x75, 0x7B, 0x69, 0x67, 0x3D, 0x33, 0x21, 0x2F,
+  0x05, 0x0B, 0x19, 0x17,
+
+  0x76, 0x78, 0x6A, 0x64, 0x4E, 0x40, 0x52, 0x5C, 0x06, 0x08, 0x1A, 0x14,
+  0x3E, 0x30, 0x22, 0x2C,
+
+  0x96, 0x98, 0x8A, 0x84, 0xAE, 0xA0, 0xB2, 0xBC, 0xE6, 0xE8, 0xFA, 0xF4,
+  0xDE, 0xD0, 0xC2, 0xCC,
+
+  0x41, 0x4F, 0x5D, 0x53, 0x79, 0x77, 0x65, 0x6B, 0x31, 0x3F, 0x2D, 0x23,
+  0x09, 0x07, 0x15, 0x1B,
+
+  0xA1, 0xAF, 0xBD, 0xB3, 0x99, 0x97, 0x85, 0x8B, 0xD1, 0xDF, 0xCD, 0xC3,
+  0xE9, 0xE7, 0xF5, 0xFB,
+
+  0x9A, 0x94, 0x86, 0x88, 0xA2, 0xAC, 0xBE, 0xB0, 0xEA, 0xE4, 0xF6, 0xF8,
+  0xD2, 0xDC, 0xCE, 0xC0,
+
+  0x7A, 0x74, 0x66, 0x68, 0x42, 0x4C, 0x5E, 0x50, 0x0A, 0x04, 0x16, 0x18,
+  0x32, 0x3C, 0x2E, 0x20,
+
+  0xEC, 0xE2, 0xF0, 0xFE, 0xD4, 0xDA, 0xC8, 0xC6, 0x9C, 0x92, 0x80, 0x8E,
+  0xA4, 0xAA, 0xB8, 0xB6,
+
+  0x0C, 0x02, 0x10, 0x1E, 0x34, 0x3A, 0x28, 0x26, 0x7C, 0x72, 0x60, 0x6E,
+  0x44, 0x4A, 0x58, 0x56,
+
+  0x37, 0x39, 0x2B, 0x25, 0x0F, 0x01, 0x13, 0x1D, 0x47, 0x49, 0x5B, 0x55,
+  0x7F, 0x71, 0x63, 0x6D,
+
+  0xD7, 0xD9, 0xCB, 0xC5, 0xEF, 0xE1, 0xF3, 0xFD, 0xA7, 0xA9, 0xBB, 0xB5,
+  0x9F, 0x91, 0x83, 0x8D
+];
diff --git a/third_party/closure/goog/crypt/blockcipher.js b/third_party/closure/goog/crypt/blockcipher.js
new file mode 100644
index 0000000..c0abe45
--- /dev/null
+++ b/third_party/closure/goog/crypt/blockcipher.js
@@ -0,0 +1,52 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Interface definition of a block cipher. A block cipher is a
+ * pair of algorithms that implement encryption and decryption of input bytes.
+ *
+ * @see http://en.wikipedia.org/wiki/Block_cipher
+ *
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+goog.provide('goog.crypt.BlockCipher');
+
+
+
+/**
+ * Interface definition for a block cipher.
+ * @interface
+ */
+goog.crypt.BlockCipher = function() {};
+
+
+/**
+ * Encrypt a plaintext block.  The implementation may expect (and assert)
+ * a particular block length.
+ * @param {!Array.<number>} input Plaintext array of input bytes.
+ * @return {!Array.<number>} Encrypted ciphertext array of bytes.  Should be the
+ *     same length as input.
+ */
+goog.crypt.BlockCipher.prototype.encrypt;
+
+
+/**
+ * Decrypt a plaintext block.  The implementation may expect (and assert)
+ * a particular block length.
+ * @param {!Array.<number>} input Ciphertext. Array of input bytes.
+ * @return {!Array.<number>} Decrypted plaintext array of bytes.  Should be the
+ *     same length as input.
+ */
+goog.crypt.BlockCipher.prototype.decrypt;
diff --git a/third_party/closure/goog/crypt/cbc.js b/third_party/closure/goog/crypt/cbc.js
new file mode 100644
index 0000000..4c59c00
--- /dev/null
+++ b/third_party/closure/goog/crypt/cbc.js
@@ -0,0 +1,150 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Implementation of CBC mode for block ciphers.  See
+ *     http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation
+ *     #Cipher-block_chaining_.28CBC.29. for description.
+ *
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+goog.provide('goog.crypt.Cbc');
+
+goog.require('goog.array');
+goog.require('goog.crypt');
+
+
+
+/**
+ * Implements the CBC mode for block ciphers. See
+ * http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation
+ * #Cipher-block_chaining_.28CBC.29
+ *
+ * @param {!goog.crypt.BlockCipher} cipher The block cipher to use.
+ * @param {number=} opt_blockSize The block size of the cipher in bytes.
+ *     Defaults to 16 bytes.
+ * @constructor
+ */
+goog.crypt.Cbc = function(cipher, opt_blockSize) {
+
+  /**
+   * Block cipher.
+   * @type {!goog.crypt.BlockCipher}
+   * @private
+   */
+  this.cipher_ = cipher;
+
+  /**
+   * Block size in bytes.
+   * @type {number}
+   * @private
+   */
+  this.blockSize_ = opt_blockSize || 16;
+};
+
+
+/**
+ * Encrypt a message.
+ *
+ * @param {!Array.<number>} plainText Message to encrypt. An array of bytes.
+ *     The length should be a multiple of the block size.
+ * @param {!Array.<number>} initialVector Initial vector for the CBC mode.
+ *     An array of bytes with the same length as the block size.
+ * @return {!Array.<number>} Encrypted message.
+ */
+goog.crypt.Cbc.prototype.encrypt = function(plainText, initialVector) {
+
+  goog.asserts.assert(
+      plainText.length % this.blockSize_ == 0,
+      'Data\'s length must be multiple of block size.');
+
+  goog.asserts.assert(
+      initialVector.length == this.blockSize_,
+      'Initial vector must be size of one block.');
+
+  // Implementation of
+  // http://en.wikipedia.org/wiki/File:Cbc_encryption.png
+
+  var cipherText = [];
+  var vector = initialVector;
+
+  // Generate each block of the encrypted cypher text.
+  for (var blockStartIndex = 0;
+       blockStartIndex < plainText.length;
+       blockStartIndex += this.blockSize_) {
+
+    // Takes one block from the input message.
+    var plainTextBlock = goog.array.slice(
+        plainText,
+        blockStartIndex,
+        blockStartIndex + this.blockSize_);
+
+    var input = goog.crypt.xorByteArray(plainTextBlock, vector);
+    var resultBlock = this.cipher_.encrypt(input);
+
+    goog.array.extend(cipherText, resultBlock);
+    vector = resultBlock;
+  }
+
+  return cipherText;
+};
+
+
+/**
+ * Decrypt a message.
+ *
+ * @param {!Array.<number>} cipherText Message to decrypt. An array of bytes.
+ *     The length should be a multiple of the block size.
+ * @param {!Array.<number>} initialVector Initial vector for the CBC mode.
+ *     An array of bytes with the same length as the block size.
+ * @return {!Array.<number>} Decrypted message.
+ */
+goog.crypt.Cbc.prototype.decrypt = function(cipherText, initialVector) {
+
+  goog.asserts.assert(
+      cipherText.length % this.blockSize_ == 0,
+      'Data\'s length must be multiple of block size.');
+
+  goog.asserts.assert(
+      initialVector.length == this.blockSize_,
+      'Initial vector must be size of one block.');
+
+  // Implementation of
+  // http://en.wikipedia.org/wiki/File:Cbc_decryption.png
+
+  var plainText = [];
+  var blockStartIndex = 0;
+  var vector = initialVector;
+
+  // Generate each block of the decrypted plain text.
+  while (blockStartIndex < cipherText.length) {
+
+    // Takes one block.
+    var cipherTextBlock = goog.array.slice(
+        cipherText,
+        blockStartIndex,
+        blockStartIndex + this.blockSize_);
+
+    var resultBlock = this.cipher_.decrypt(cipherTextBlock);
+    var plainTextBlock = goog.crypt.xorByteArray(vector, resultBlock);
+
+    goog.array.extend(plainText, plainTextBlock);
+    vector = cipherTextBlock;
+
+    blockStartIndex += this.blockSize_;
+  }
+
+  return plainText;
+};
diff --git a/third_party/closure/goog/crypt/crypt.js b/third_party/closure/goog/crypt/crypt.js
index 67f3b56..20b3069 100644
--- a/third_party/closure/goog/crypt/crypt.js
+++ b/third_party/closure/goog/crypt/crypt.js
@@ -25,7 +25,7 @@
  * Turns a string into an array of bytes; a "byte" being a JS number in the
  * range 0-255.
  * @param {string} str String value to arrify.
- * @return {Array.<number>} Array of numbers corresponding to the
+ * @return {!Array.<number>} Array of numbers corresponding to the
  *     UCS character codes of each character in str.
  */
 goog.crypt.stringToByteArray = function(str) {
@@ -68,6 +68,23 @@
 
 
 /**
+ * Converts a hex string into an integer array.
+ * @param {string} hexString Hex string of 16-bit integers (two characters
+ *     per integer).
+ * @return {!Array.<number>} Array of {0,255} integers for the given string.
+ */
+goog.crypt.hexToByteArray = function(hexString) {
+  goog.asserts.assert(hexString.length % 2 == 0,
+                      'Key string length must be multiple of 2');
+  var arr = [];
+  for (var i = 0; i < hexString.length; i += 2) {
+    arr.push(parseInt(hexString.substring(i, i + 2), 16));
+  }
+  return arr;
+};
+
+
+/**
  * Converts a JS string to a UTF-8 "byte" array.
  * @param {string} str 16-bit unicode string.
  * @return {Array.<number>} UTF-8 byte array.
@@ -117,3 +134,22 @@
   }
   return out.join('');
 };
+
+
+/**
+ * XOR two byte arrays.
+ * @param {!Array.<number>} bytes1 Byte array 1.
+ * @param {!Array.<number>} bytes2 Byte array 2.
+ * @return {!Array.<number>} Resulting XOR of the two byte arrays.
+ */
+goog.crypt.xorByteArray = function(bytes1, bytes2) {
+  goog.asserts.assert(
+      bytes1.length == bytes2.length,
+      'XOR array lengths must match');
+
+  var result = [];
+  for (var i = 0; i < bytes1.length; i++) {
+    result.push(bytes1[i] ^ bytes2[i]);
+  }
+  return result;
+};
diff --git a/third_party/closure/goog/crypt/hash.js b/third_party/closure/goog/crypt/hash.js
index 713e4a1..941ebfd 100644
--- a/third_party/closure/goog/crypt/hash.js
+++ b/third_party/closure/goog/crypt/hash.js
@@ -48,7 +48,7 @@
 
 
 /**
- * @return {Array.<number>} The finalized hash computed
+ * @return {!Array.<number>} The finalized hash computed
  *     from the internal accumulator.
  */
 goog.crypt.Hash.prototype.digest = goog.abstractMethod;
diff --git a/third_party/closure/goog/crypt/hash_test.js b/third_party/closure/goog/crypt/hash_test.js
index bbd0fd6..5944277 100644
--- a/third_party/closure/goog/crypt/hash_test.js
+++ b/third_party/closure/goog/crypt/hash_test.js
@@ -19,6 +19,7 @@
 
 goog.provide('goog.crypt.hash_test');
 
+goog.require('goog.array');
 goog.require('goog.testing.asserts');
 goog.setTestOnly('hash_test');
 
@@ -81,3 +82,66 @@
   assertArrayEquals('Updating with an empty string did not give an empty hash',
       empty, hash.digest());
 };
+
+
+// Run performance tests
+goog.crypt.hash_test.runPerfTests = function (hashFactory, hashName) {
+
+  var body = goog.dom.getDocument().body;
+  var perfTable = goog.dom.createElement('div');
+  goog.dom.appendChild(body, perfTable);
+
+  var table = new goog.testing.PerformanceTable(perfTable)
+
+  function runPerfTest(byteLength, updateCount) {
+
+    var label = (hashName + ': ' + updateCount + ' update(s) of ' + byteLength
+        + ' bytes');
+
+    function run(data, dataType) {
+      table.run(function() {
+        var hash = hashFactory();
+        for (var i = 0; i < updateCount; i++) {
+          hash.update(data, byteLength);
+        }
+        var digest = hash.digest();
+      }, label + ' (' + dataType + ')');
+    }
+
+    var byteArray = goog.crypt.hash_test.createRandomByteArray_(length);
+    var byteString = goog.crypt.hash_test.createByteString_(byteArray);
+
+    run(byteArray, 'byte array');
+    run(byteString, 'byte string');
+  }
+
+  var MESSAGE_LENGTH_LONG = 10000002;
+  var MESSAGE_LENGTH_SHORT = 1000002;
+
+  runPerfTest(MESSAGE_LENGTH_SHORT, 1);
+  runPerfTest(MESSAGE_LENGTH_LONG, 1);
+  runPerfTest(MESSAGE_LENGTH_SHORT, 10);
+};
+
+
+goog.crypt.hash_test.createRandomByteArray_ = function(length) {
+  var random = new goog.testing.PseudoRandom(0);
+  var bytes = [];
+
+  for (var i = 0; i < length; ++i) {
+    // Generates an integer from 0 to 255.
+    var byte = Math.floor(random.random() * 0x100);
+    bytes.push(byte);
+  }
+
+  return bytes;
+};
+
+
+goog.crypt.hash_test.createByteString_ = function(bytes) {
+  var str = '';
+  goog.array.forEach(bytes, function(byte) {
+    str += String.fromCharCode(byte);
+  });
+  return str;
+}
diff --git a/third_party/closure/goog/crypt/hmac.js b/third_party/closure/goog/crypt/hmac.js
index 7d4cfb7..9d181dc 100644
--- a/third_party/closure/goog/crypt/hmac.js
+++ b/third_party/closure/goog/crypt/hmac.js
@@ -153,11 +153,10 @@
  * Calculates an HMAC for a given message.
  *
  * @param {Array.<number>} message  An array of integers in {0, 255}.
- * @return {Array} the digest of the given message.
+ * @return {!Array.<number>} the digest of the given message.
  */
 goog.crypt.Hmac.prototype.getHmac = function(message) {
   this.reset();
   this.update(message);
   return this.digest();
 };
-
diff --git a/third_party/closure/goog/crypt/pbkdf2.js b/third_party/closure/goog/crypt/pbkdf2.js
new file mode 100644
index 0000000..b1b8629
--- /dev/null
+++ b/third_party/closure/goog/crypt/pbkdf2.js
@@ -0,0 +1,127 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Implementation of PBKDF2 in JavaScript.
+ * @see http://en.wikipedia.org/wiki/PBKDF2
+ *
+ * Currently we only support HMAC-SHA1 as the underlying hash function. To add a
+ * new hash function, add a static method similar to deriveKeyFromPasswordSha1()
+ * and implement the specific computeBlockCallback() using the hash function.
+ *
+ * Usage:
+ *   var key = pbkdf2.deriveKeySha1(
+ *       stringToByteArray('password'), stringToByteArray('salt'), 1000, 128);
+ *
+ */
+
+goog.provide('goog.crypt.pbkdf2');
+
+goog.require('goog.asserts');
+goog.require('goog.crypt');
+goog.require('goog.crypt.Hmac');
+goog.require('goog.crypt.Sha1');
+
+
+/**
+ * Derives key from password using PBKDF2-SHA1
+ * @param {!Array.<number>} password Byte array representation of the password
+ *     from which the key is derived.
+ * @param {!Array.<number>} initialSalt Byte array representation of the salt.
+ * @param {number} iterations Number of interations when computing the key.
+ * @param {number} keyLength Length of the output key in bits.
+ *     Must be multiple of 8.
+ * @return {!Array.<number>} Byte array representation of the output key.
+ */
+goog.crypt.pbkdf2.deriveKeySha1 = function(
+    password, initialSalt, iterations, keyLength) {
+  // Length of the HMAC-SHA1 output in bits.
+  var HASH_LENGTH = 160;
+
+  /**
+   * Compute each block of the key using HMAC-SHA1.
+   * @param {!Array.<number>} index Byte array representation of the index of
+   *     the block to be computed.
+   * @return {!Array.<number>} Byte array representation of the output block.
+   */
+  var computeBlock = function(index) {
+    // Initialize the result to be array of 0 such that its xor with the first
+    // block would be the first block.
+    var result = goog.array.repeat(0, HASH_LENGTH / 8);
+    // Initialize the salt of the first iteration to initialSalt || i.
+    var salt = initialSalt.concat(index);
+    var hmac = new goog.crypt.Hmac(new goog.crypt.Sha1(), password, 64);
+    // Compute and XOR each iteration.
+    for (var i = 0; i < iterations; i++) {
+      // The salt of the next iteration is the result of the current iteration.
+      salt = hmac.getHmac(salt);
+      result = goog.crypt.xorByteArray(result, salt);
+    }
+    return result;
+  };
+
+  return goog.crypt.pbkdf2.deriveKeyFromPassword_(
+      computeBlock, HASH_LENGTH, keyLength);
+};
+
+
+/**
+ * Compute each block of the key using PBKDF2.
+ * @param {Function} computeBlock Function to compute each block of the output
+ *     key.
+ * @param {number} hashLength Length of each block in bits. This is determined
+ *     by the specific hash function used. Must be multiple of 8.
+ * @param {number} keyLength Length of the output key in bits.
+ *     Must be multiple of 8.
+ * @return {!Array.<number>} Byte array representation of the output key.
+ * @private
+ */
+goog.crypt.pbkdf2.deriveKeyFromPassword_ =
+    function(computeBlock, hashLength, keyLength) {
+  goog.asserts.assert(keyLength % 8 == 0, 'invalid output key length');
+
+  // Compute and concactate each block of the output key.
+  var numBlocks = Math.ceil(keyLength / hashLength);
+  goog.asserts.assert(numBlocks >= 1, 'invalid number of blocks');
+  var result = [];
+  for (var i = 1; i <= numBlocks; i++) {
+    var indexBytes = goog.crypt.pbkdf2.integerToByteArray_(i);
+    result = result.concat(computeBlock(indexBytes));
+  }
+
+  // Trim the last block if needed.
+  var lastBlockSize = keyLength % hashLength;
+  if (lastBlockSize != 0) {
+    var desiredBytes = ((numBlocks - 1) * hashLength + lastBlockSize) / 8;
+    result.splice(desiredBytes, (hashLength - lastBlockSize) / 8);
+  }
+  return result;
+};
+
+
+/**
+ * Converts an integer number to a 32-bit big endian byte array.
+ * @param {number} n Integer number to be converted.
+ * @return {!Array.<number>} Byte Array representation of the 32-bit big endian
+ *     encoding of n.
+ * @private
+ */
+goog.crypt.pbkdf2.integerToByteArray_ = function(n) {
+  var result = new Array(4);
+  result[0] = n >> 24 & 0xFF;
+  result[1] = n >> 16 & 0xFF;
+  result[2] = n >> 8 & 0xFF;
+  result[3] = n & 0xFF;
+  return result;
+};
diff --git a/third_party/closure/goog/date/relative.js b/third_party/closure/goog/date/relative.js
index be8b642..27ca2cf 100644
--- a/third_party/closure/goog/date/relative.js
+++ b/third_party/closure/goog/date/relative.js
@@ -208,9 +208,11 @@
  * "Yesterday" or "Sept 15".
  *
  * @param {number} dateMs Date in milliseconds.
+ * @param {!function(!Date):string=} opt_formatter Formatter for the date.
+ *     Defaults to form 'MMM dd'.
  * @return {string} The formatted date.
  */
-goog.date.relative.formatDay = function(dateMs) {
+goog.date.relative.formatDay = function(dateMs, opt_formatter) {
   var message;
   var today = new Date(goog.now());
 
@@ -229,7 +231,8 @@
     var MSG_YESTERDAY = goog.getMsg('Yesterday');
     message = MSG_YESTERDAY;
   } else {
-    message = goog.date.relative.formatMonth_(new Date(dateMs));
+    var formatFunction = opt_formatter || goog.date.relative.formatMonth_;
+    message = formatFunction(new Date(dateMs));
   }
   return message;
 };
diff --git a/third_party/closure/goog/db/indexeddb.js b/third_party/closure/goog/db/indexeddb.js
index ed27d16..afc4fe1 100644
--- a/third_party/closure/goog/db/indexeddb.js
+++ b/third_party/closure/goog/db/indexeddb.js
@@ -95,7 +95,7 @@
 
 
 /**
- * @return {Array} List of object stores in this database.
+ * @return {DOMStringList} List of object stores in this database.
  */
 goog.db.IndexedDb.prototype.getObjectStoreNames = function() {
   return this.db_.objectStoreNames;
diff --git a/third_party/closure/goog/demos/inputhandler.html b/third_party/closure/goog/demos/inputhandler.html
index 380d0d4..8c0e938 100644
--- a/third_party/closure/goog/demos/inputhandler.html
+++ b/third_party/closure/goog/demos/inputhandler.html
@@ -10,6 +10,8 @@
 <title>goog.events.InputHandler</title>
 <script src="../base.js"></script>
 <script>
+goog.require('goog.debug.DivConsole');
+goog.require('goog.debug.Logger');
 goog.require('goog.dom');
 goog.require('goog.events');
 goog.require('goog.events.InputHandler');
@@ -19,19 +21,38 @@
 <body>
 <h1>goog.events.InputHandler</h1>
 <p><button onclick="addSome('text')">Add Some</button>
-<input type=text id=text><span></span>
-
+<input type="text" id="text"><span></span>
 
 <p><button onclick="addSome('password')">Add Some</button>
-<input type=password id=password><span></span>
+<input type="password" id="password"><span></span>
 
 
 <p><button onclick="addSome('textarea')">Add Some</button>
-<textarea id=textarea></textarea><span></span>
+<textarea id="textarea"></textarea><span></span>
+
+<br><br>
+<fieldset class="goog-debug-panel">
+  <legend>Event Log</legend>
+  <div id="log" style="height: 400px;"></div>
+</fieldset>
 
 <script>
-
 var $ = goog.dom.getElement;
+var logger = goog.debug.Logger.getLogger('demo');
+var logconsole = new goog.debug.DivConsole(goog.dom.getElement('log'));
+
+goog.debug.LogManager.getRoot().setLevel(goog.debug.Logger.Level.FINER);
+logconsole.setCapturing(true);
+
+var DOM_EVENTS = ['keydown', 'keyup', 'keypress', 'change', 'cut', 'paste',
+    'drop', 'input'];
+var INPUTHANDLER_EVENTS =
+    goog.object.getValues(goog.events.InputHandler.EventType);
+
+
+function logEvent(src, e) {
+  logger.info('[Event from ' + src + ']:  ' + e.type);
+}
 
 function addSome(id) {
   var el = $(id);
@@ -47,26 +68,21 @@
   outputEl.appendChild(document.createTextNode(inputEl.value));
 }
 
-var textIh = new goog.events.InputHandler($('text'));
-var passwordIh = new goog.events.InputHandler($('password'));
-var textareaIh = new goog.events.InputHandler($('textarea'));
 
-goog.events.listen(textIh, goog.events.InputHandler.EventType.INPUT,
-                   updateText);
-goog.events.listen(passwordIh, goog.events.InputHandler.EventType.INPUT,
-                   updateText);
-goog.events.listen(textareaIh, goog.events.InputHandler.EventType.INPUT,
-                   updateText);
+goog.object.forEach(['text', 'password', 'textarea'], function(id) {
+  var el = $(id);
+  var ih = new goog.events.InputHandler(el);
 
-goog.events.listen(window, 'unload', function() {
-  goog.events.unlisten(textIh, goog.events.InputHandler.EventType.INPUT,
-                       updateText);
-  goog.events.unlisten(passwordIh, goog.events.InputHandler.EventType.INPUT,
-                       updateText);
-  goog.events.unlisten(textareaIh, goog.events.InputHandler.EventType.INPUT,
-                       updateText);
+  goog.events.listen(ih, goog.events.InputHandler.EventType.INPUT, updateText);
+  goog.events.listen(ih, INPUTHANDLER_EVENTS,
+      goog.partial(logEvent, 'InputHandler'));
+  goog.events.listen(el, DOM_EVENTS,
+      goog.partial(logEvent, 'DOM'));
 });
 
+goog.events.listen(window, 'unload', function() {
+  goog.events.removeAll();
+});
 </script>
 </body>
 </html>
diff --git a/third_party/closure/goog/deps.js b/third_party/closure/goog/deps.js
index 2d835dc..7b0b267 100644
--- a/third_party/closure/goog/deps.js
+++ b/third_party/closure/goog/deps.js
@@ -35,16 +35,21 @@
 goog.addDependency('color/alpha.js', ['goog.color.alpha'], ['goog.color']);
 goog.addDependency('color/color.js', ['goog.color'], ['goog.color.names', 'goog.math']);
 goog.addDependency('color/names.js', ['goog.color.names'], []);
+goog.addDependency('crypt/aes.js', ['goog.crypt.Aes'], ['goog.asserts', 'goog.crypt.BlockCipher']);
 goog.addDependency('crypt/arc4.js', ['goog.crypt.Arc4'], ['goog.asserts']);
 goog.addDependency('crypt/base64.js', ['goog.crypt.base64'], ['goog.crypt', 'goog.userAgent']);
 goog.addDependency('crypt/basen.js', ['goog.crypt.baseN'], []);
 goog.addDependency('crypt/blobhasher.js', ['goog.crypt.BlobHasher', 'goog.crypt.BlobHasher.EventType'], ['goog.asserts', 'goog.crypt', 'goog.crypt.Hash', 'goog.debug.Logger', 'goog.events.EventTarget', 'goog.fs']);
+goog.addDependency('crypt/blockcipher.js', ['goog.crypt.BlockCipher'], []);
+goog.addDependency('crypt/cbc.js', ['goog.crypt.Cbc'], ['goog.array', 'goog.crypt']);
+goog.addDependency('crypt/cbc_test.js', ['goog.crypt.CbcTest'], ['goog.crypt', 'goog.crypt.Aes', 'goog.crypt.Cbc', 'goog.testing.jsunit']);
 goog.addDependency('crypt/crypt.js', ['goog.crypt'], ['goog.array']);
 goog.addDependency('crypt/hash.js', ['goog.crypt.Hash'], []);
 goog.addDependency('crypt/hash32.js', ['goog.crypt.hash32'], ['goog.crypt']);
-goog.addDependency('crypt/hash_test.js', ['goog.crypt.hash_test'], ['goog.testing.asserts']);
+goog.addDependency('crypt/hash_test.js', ['goog.crypt.hash_test'], ['goog.array', 'goog.testing.asserts']);
 goog.addDependency('crypt/hmac.js', ['goog.crypt.Hmac'], ['goog.asserts', 'goog.crypt.Hash']);
 goog.addDependency('crypt/md5.js', ['goog.crypt.Md5'], ['goog.crypt.Hash']);
+goog.addDependency('crypt/pbkdf2.js', ['goog.crypt.pbkdf2'], ['goog.asserts', 'goog.crypt', 'goog.crypt.Hmac', 'goog.crypt.Sha1']);
 goog.addDependency('crypt/sha1.js', ['goog.crypt.Sha1'], ['goog.crypt.Hash']);
 goog.addDependency('cssom/cssom.js', ['goog.cssom', 'goog.cssom.CssRuleType'], ['goog.array', 'goog.dom']);
 goog.addDependency('cssom/iframe/style.js', ['goog.cssom.iframe.style'], ['goog.cssom', 'goog.dom', 'goog.dom.NodeType', 'goog.dom.classes', 'goog.string', 'goog.style', 'goog.userAgent']);
@@ -105,6 +110,8 @@
 goog.addDependency('dom/browserrange/operarange.js', ['goog.dom.browserrange.OperaRange'], ['goog.dom.browserrange.W3cRange']);
 goog.addDependency('dom/browserrange/w3crange.js', ['goog.dom.browserrange.W3cRange'], ['goog.dom', 'goog.dom.NodeType', 'goog.dom.RangeEndpoint', 'goog.dom.browserrange.AbstractRange', 'goog.string']);
 goog.addDependency('dom/browserrange/webkitrange.js', ['goog.dom.browserrange.WebKitRange'], ['goog.dom.RangeEndpoint', 'goog.dom.browserrange.W3cRange', 'goog.userAgent']);
+goog.addDependency('dom/bufferedviewportsizemonitor.js', ['goog.dom.BufferedViewportSizeMonitor'], ['goog.asserts', 'goog.async.Delay', 'goog.events', 'goog.events.EventTarget', 'goog.events.EventType']);
+goog.addDependency('dom/bufferedviewportsizemonitor_test.js', ['goog.dom.BufferedViewportSizeMonitorTest'], ['goog.dom.BufferedViewportSizeMonitor', 'goog.dom.ViewportSizeMonitor', 'goog.events', 'goog.events.EventType', 'goog.math.Size', 'goog.testing.MockClock', 'goog.testing.events', 'goog.testing.events.Event', 'goog.testing.jsunit']);
 goog.addDependency('dom/classes.js', ['goog.dom.classes'], ['goog.array']);
 goog.addDependency('dom/classes_test.js', ['goog.dom.classes_test'], ['goog.dom', 'goog.dom.classes', 'goog.testing.jsunit']);
 goog.addDependency('dom/controlrange.js', ['goog.dom.ControlRange', 'goog.dom.ControlRangeIterator'], ['goog.array', 'goog.dom', 'goog.dom.AbstractMultiRange', 'goog.dom.AbstractRange', 'goog.dom.RangeIterator', 'goog.dom.RangeType', 'goog.dom.SavedRange', 'goog.dom.TagWalkType', 'goog.dom.TextRange', 'goog.iter.StopIteration', 'goog.userAgent']);
@@ -151,14 +158,14 @@
 goog.addDependency('editor/command.js', ['goog.editor.Command'], []);
 goog.addDependency('editor/contenteditablefield.js', ['goog.editor.ContentEditableField'], ['goog.asserts', 'goog.debug.Logger', 'goog.editor.Field']);
 goog.addDependency('editor/defines.js', ['goog.editor.defines'], []);
-goog.addDependency('editor/field.js', ['goog.editor.Field', 'goog.editor.Field.EventType'], ['goog.array', 'goog.async.Delay', 'goog.debug.Logger', 'goog.dom', 'goog.dom.Range', 'goog.dom.TagName', 'goog.dom.classes', 'goog.editor.BrowserFeature', 'goog.editor.Command', 'goog.editor.Plugin', 'goog.editor.icontent', 'goog.editor.icontent.FieldFormatInfo', 'goog.editor.icontent.FieldStyleInfo', 'goog.editor.node', 'goog.editor.range', 'goog.events', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.functions', 'goog.string', 'goog.string.Unicode', 'goog.style', 'goog.userAgent', 'goog.userAgent.product']);
+goog.addDependency('editor/field.js', ['goog.editor.Field', 'goog.editor.Field.EventType'], ['goog.array', 'goog.asserts', 'goog.async.Delay', 'goog.debug.Logger', 'goog.dom', 'goog.dom.Range', 'goog.dom.TagName', 'goog.editor.BrowserFeature', 'goog.editor.Command', 'goog.editor.Plugin', 'goog.editor.icontent', 'goog.editor.icontent.FieldFormatInfo', 'goog.editor.icontent.FieldStyleInfo', 'goog.editor.node', 'goog.editor.range', 'goog.events', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.functions', 'goog.string', 'goog.string.Unicode', 'goog.style', 'goog.userAgent', 'goog.userAgent.product']);
 goog.addDependency('editor/field_test.js', ['goog.editor.field_test'], ['goog.dom.Range', 'goog.editor.Command', 'goog.editor.Field', 'goog.editor.Plugin', 'goog.events', 'goog.events.KeyCodes', 'goog.functions', 'goog.testing.LooseMock', 'goog.testing.MockClock', 'goog.testing.dom', 'goog.testing.events', 'goog.testing.recordFunction', 'goog.userAgent', 'goog.userAgent.product']);
 goog.addDependency('editor/focus.js', ['goog.editor.focus'], ['goog.dom.selection']);
 goog.addDependency('editor/icontent.js', ['goog.editor.icontent', 'goog.editor.icontent.FieldFormatInfo', 'goog.editor.icontent.FieldStyleInfo'], ['goog.editor.BrowserFeature', 'goog.style', 'goog.userAgent']);
 goog.addDependency('editor/link.js', ['goog.editor.Link'], ['goog.array', 'goog.dom', 'goog.dom.NodeType', 'goog.dom.Range', 'goog.editor.BrowserFeature', 'goog.editor.Command', 'goog.editor.node', 'goog.editor.range', 'goog.string', 'goog.string.Unicode', 'goog.uri.utils', 'goog.uri.utils.ComponentIndex']);
 goog.addDependency('editor/node.js', ['goog.editor.node'], ['goog.dom', 'goog.dom.NodeType', 'goog.dom.TagName', 'goog.dom.iter.ChildIterator', 'goog.dom.iter.SiblingIterator', 'goog.iter', 'goog.object', 'goog.string', 'goog.string.Unicode']);
 goog.addDependency('editor/plugin.js', ['goog.editor.Plugin'], ['goog.debug.Logger', 'goog.editor.Command', 'goog.events.EventTarget', 'goog.functions', 'goog.object', 'goog.reflect']);
-goog.addDependency('editor/plugins/abstractbubbleplugin.js', ['goog.editor.plugins.AbstractBubblePlugin'], ['goog.dom', 'goog.dom.NodeType', 'goog.dom.Range', 'goog.dom.TagName', 'goog.editor.Plugin', 'goog.editor.style', 'goog.events', 'goog.events.EventHandler', 'goog.events.EventType', 'goog.functions', 'goog.string.Unicode', 'goog.ui.Component.EventType', 'goog.ui.editor.Bubble', 'goog.userAgent']);
+goog.addDependency('editor/plugins/abstractbubbleplugin.js', ['goog.editor.plugins.AbstractBubblePlugin'], ['goog.dom', 'goog.dom.NodeType', 'goog.dom.Range', 'goog.dom.TagName', 'goog.editor.Plugin', 'goog.editor.style', 'goog.events', 'goog.events.EventHandler', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.actionEventWrapper', 'goog.functions', 'goog.string.Unicode', 'goog.ui.Component.EventType', 'goog.ui.editor.Bubble', 'goog.userAgent']);
 goog.addDependency('editor/plugins/abstractdialogplugin.js', ['goog.editor.plugins.AbstractDialogPlugin', 'goog.editor.plugins.AbstractDialogPlugin.EventType'], ['goog.dom', 'goog.dom.Range', 'goog.editor.Field.EventType', 'goog.editor.Plugin', 'goog.editor.range', 'goog.events', 'goog.ui.editor.AbstractDialog.EventType']);
 goog.addDependency('editor/plugins/abstracttabhandler.js', ['goog.editor.plugins.AbstractTabHandler'], ['goog.editor.Plugin', 'goog.events.KeyCodes']);
 goog.addDependency('editor/plugins/basictextformatter.js', ['goog.editor.plugins.BasicTextFormatter', 'goog.editor.plugins.BasicTextFormatter.COMMAND'], ['goog.array', 'goog.debug.Logger', 'goog.dom', 'goog.dom.NodeType', 'goog.dom.Range', 'goog.dom.TagName', 'goog.editor.BrowserFeature', 'goog.editor.Command', 'goog.editor.Link', 'goog.editor.Plugin', 'goog.editor.node', 'goog.editor.range', 'goog.editor.style', 'goog.iter', 'goog.iter.StopIteration', 'goog.object', 'goog.string', 'goog.string.Unicode', 'goog.style', 'goog.ui.editor.messages', 'goog.userAgent']);
@@ -193,6 +200,7 @@
 goog.addDependency('events/eventhandler.js', ['goog.events.EventHandler'], ['goog.Disposable', 'goog.array', 'goog.events', 'goog.events.EventWrapper']);
 goog.addDependency('events/events.js', ['goog.events'], ['goog.array', 'goog.debug.entryPointRegistry', 'goog.debug.errorHandlerWeakDep', 'goog.events.BrowserEvent', 'goog.events.BrowserFeature', 'goog.events.Event', 'goog.events.EventWrapper', 'goog.events.Listener', 'goog.object', 'goog.userAgent']);
 goog.addDependency('events/eventtarget.js', ['goog.events.EventTarget'], ['goog.Disposable', 'goog.events']);
+goog.addDependency('events/eventtargettester.js', ['goog.events.eventTargetTester', 'goog.events.eventTargetTester.KeyType', 'goog.events.eventTargetTester.UnlistenReturnType'], ['goog.events.Event', 'goog.events.EventTarget', 'goog.testing.asserts', 'goog.testing.recordFunction']);
 goog.addDependency('events/eventtype.js', ['goog.events.EventType'], ['goog.userAgent']);
 goog.addDependency('events/eventwrapper.js', ['goog.events.EventWrapper'], []);
 goog.addDependency('events/filedrophandler.js', ['goog.events.FileDropHandler', 'goog.events.FileDropHandler.EventType'], ['goog.array', 'goog.debug.Logger', 'goog.dom', 'goog.events', 'goog.events.BrowserEvent', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.events.EventType']);
@@ -216,7 +224,7 @@
 goog.addDependency('fs/filesaver.js', ['goog.fs.FileSaver', 'goog.fs.FileSaver.EventType', 'goog.fs.FileSaver.ProgressEvent', 'goog.fs.FileSaver.ReadyState'], ['goog.events.Event', 'goog.events.EventTarget', 'goog.fs.Error', 'goog.fs.ProgressEvent']);
 goog.addDependency('fs/filesystem.js', ['goog.fs.FileSystem'], ['goog.fs.DirectoryEntry']);
 goog.addDependency('fs/filewriter.js', ['goog.fs.FileWriter'], ['goog.fs.Error', 'goog.fs.FileSaver']);
-goog.addDependency('fs/fs.js', ['goog.fs'], ['goog.async.Deferred', 'goog.events', 'goog.fs.Error', 'goog.fs.FileReader', 'goog.fs.FileSystem', 'goog.userAgent']);
+goog.addDependency('fs/fs.js', ['goog.fs'], ['goog.array', 'goog.async.Deferred', 'goog.events', 'goog.fs.Error', 'goog.fs.FileReader', 'goog.fs.FileSystem', 'goog.userAgent']);
 goog.addDependency('fs/progressevent.js', ['goog.fs.ProgressEvent'], ['goog.events.Event']);
 goog.addDependency('functions/functions.js', ['goog.functions'], []);
 goog.addDependency('fx/abstractdragdrop.js', ['goog.fx.AbstractDragDrop', 'goog.fx.AbstractDragDrop.EventType', 'goog.fx.DragDropEvent', 'goog.fx.DragDropItem'], ['goog.dom', 'goog.dom.classes', 'goog.events', 'goog.events.Event', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.fx.Dragger', 'goog.fx.Dragger.EventType', 'goog.math.Box', 'goog.math.Coordinate', 'goog.style']);
@@ -319,9 +327,10 @@
 goog.addDependency('json/json.js', ['goog.json', 'goog.json.Serializer'], []);
 goog.addDependency('json/nativejsonprocessor.js', ['goog.json.NativeJsonProcessor'], ['goog.asserts', 'goog.json', 'goog.json.Processor']);
 goog.addDependency('json/processor.js', ['goog.json.Processor'], ['goog.string.Parser', 'goog.string.Stringifier']);
-goog.addDependency('labs/mock/mock.js', ['goog.labs.mock'], ['goog.array']);
+goog.addDependency('labs/classdef/classdef.js', ['goog.labs.classdef'], []);
+goog.addDependency('labs/mock/mock.js', ['goog.labs.mock'], ['goog.array', 'goog.debug.Error', 'goog.functions', 'goog.testing.recordFunction']);
 goog.addDependency('labs/net/image.js', ['goog.labs.net.image'], ['goog.events.EventHandler', 'goog.events.EventType', 'goog.net.EventType', 'goog.result.SimpleResult', 'goog.userAgent']);
-goog.addDependency('labs/net/image_test.js', ['goog.labs.net.imageTest'], ['goog.events', 'goog.labs.net.image', 'goog.net.EventType', 'goog.result', 'goog.result.Result', 'goog.string', 'goog.testing.AsyncTestCase', 'goog.testing.jsunit', 'goog.testing.recordFunction']);
+goog.addDependency('labs/net/image_test.js', ['goog.labs.net.imageTest'], ['goog.events', 'goog.labs.net.image', 'goog.result', 'goog.result.Result', 'goog.string', 'goog.testing.AsyncTestCase', 'goog.testing.jsunit', 'goog.testing.recordFunction']);
 goog.addDependency('labs/net/xhr.js', ['goog.labs.net.xhr', 'goog.labs.net.xhr.Error', 'goog.labs.net.xhr.HttpError', 'goog.labs.net.xhr.TimeoutError'], ['goog.debug.Error', 'goog.json', 'goog.net.HttpStatus', 'goog.net.XmlHttp', 'goog.result', 'goog.string', 'goog.uri.utils']);
 goog.addDependency('labs/object/object.js', ['goog.labs.object'], []);
 goog.addDependency('labs/observe/notice.js', ['goog.labs.observe.Notice'], []);
@@ -340,7 +349,7 @@
 goog.addDependency('labs/testing/matcher.js', ['goog.labs.testing.Matcher'], []);
 goog.addDependency('labs/testing/numbermatcher.js', ['goog.labs.testing.CloseToMatcher', 'goog.labs.testing.EqualToMatcher', 'goog.labs.testing.GreaterThanEqualToMatcher', 'goog.labs.testing.GreaterThanMatcher', 'goog.labs.testing.LessThanEqualToMatcher', 'goog.labs.testing.LessThanMatcher'], ['goog.asserts', 'goog.labs.testing.Matcher']);
 goog.addDependency('labs/testing/objectmatcher.js', ['goog.labs.testing.HasPropertyMatcher', 'goog.labs.testing.InstanceOfMatcher', 'goog.labs.testing.IsNullMatcher', 'goog.labs.testing.IsNullOrUndefinedMatcher', 'goog.labs.testing.IsUndefinedMatcher', 'goog.labs.testing.ObjectEqualsMatcher'], ['goog.labs.testing.Matcher', 'goog.string']);
-goog.addDependency('labs/testing/stringmatcher.js', ['goog.labs.testing.ContainsStringMatcher', 'goog.labs.testing.EndsWithMatcher', 'goog.labs.testing.EqualToIgnoringCaseMatcher', 'goog.labs.testing.EqualToIgnoringWhitespaceMatcher', 'goog.labs.testing.EqualsMatcher', 'goog.labs.testing.StartsWithMatcher', 'goog.labs.testing.StringContainsInOrderMatcher'], ['goog.asserts', 'goog.labs.testing.Matcher', 'goog.string']);
+goog.addDependency('labs/testing/stringmatcher.js', ['goog.labs.testing.ContainsStringMatcher', 'goog.labs.testing.EndsWithMatcher', 'goog.labs.testing.EqualToIgnoringCaseMatcher', 'goog.labs.testing.EqualToIgnoringWhitespaceMatcher', 'goog.labs.testing.EqualsMatcher', 'goog.labs.testing.RegexMatcher', 'goog.labs.testing.StartsWithMatcher', 'goog.labs.testing.StringContainsInOrderMatcher'], ['goog.asserts', 'goog.labs.testing.Matcher', 'goog.string']);
 goog.addDependency('locale/countries.js', ['goog.locale.countries'], []);
 goog.addDependency('locale/defaultlocalenameconstants.js', ['goog.locale.defaultLocaleNameConstants'], []);
 goog.addDependency('locale/genericfontnames.js', ['goog.locale.genericFontNames'], []);
@@ -396,7 +405,7 @@
 goog.addDependency('module/moduleinfo.js', ['goog.module.ModuleInfo'], ['goog.Disposable', 'goog.functions', 'goog.module.BaseModule', 'goog.module.ModuleLoadCallback']);
 goog.addDependency('module/moduleloadcallback.js', ['goog.module.ModuleLoadCallback'], ['goog.debug.entryPointRegistry', 'goog.debug.errorHandlerWeakDep']);
 goog.addDependency('module/moduleloader.js', ['goog.module.ModuleLoader'], ['goog.Timer', 'goog.array', 'goog.debug.Logger', 'goog.events', 'goog.events.Event', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.module.AbstractModuleLoader', 'goog.net.BulkLoader', 'goog.net.EventType', 'goog.net.jsloader']);
-goog.addDependency('module/modulemanager.js', ['goog.module.ModuleManager', 'goog.module.ModuleManager.CallbackType', 'goog.module.ModuleManager.FailureType'], ['goog.Disposable', 'goog.array', 'goog.asserts', 'goog.async.Deferred', 'goog.debug.Logger', 'goog.debug.Trace', 'goog.module.ModuleInfo', 'goog.module.ModuleLoadCallback', 'goog.object']);
+goog.addDependency('module/modulemanager.js', ['goog.module.ModuleManager', 'goog.module.ModuleManager.CallbackType', 'goog.module.ModuleManager.FailureType'], ['goog.Disposable', 'goog.array', 'goog.asserts', 'goog.async.Deferred', 'goog.debug.Logger', 'goog.debug.Trace', 'goog.dispose', 'goog.module.ModuleInfo', 'goog.module.ModuleLoadCallback', 'goog.object']);
 goog.addDependency('module/testdata/modA_1.js', ['goog.module.testdata.modA_1'], []);
 goog.addDependency('module/testdata/modA_2.js', ['goog.module.testdata.modA_2'], ['goog.module.ModuleManager']);
 goog.addDependency('module/testdata/modB_1.js', ['goog.module.testdata.modB_1'], ['goog.module.ModuleManager']);
@@ -405,14 +414,14 @@
 goog.addDependency('net/bulkloader.js', ['goog.net.BulkLoader'], ['goog.debug.Logger', 'goog.events.Event', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.net.BulkLoaderHelper', 'goog.net.EventType', 'goog.net.XhrIo']);
 goog.addDependency('net/bulkloaderhelper.js', ['goog.net.BulkLoaderHelper'], ['goog.Disposable', 'goog.debug.Logger']);
 goog.addDependency('net/channeldebug.js', ['goog.net.ChannelDebug'], ['goog.debug.Logger', 'goog.json']);
-goog.addDependency('net/channelrequest.js', ['goog.net.ChannelRequest', 'goog.net.ChannelRequest.Error'], ['goog.Timer', 'goog.events', 'goog.events.EventHandler', 'goog.net.EventType', 'goog.net.XmlHttp.ReadyState', 'goog.object', 'goog.userAgent']);
-goog.addDependency('net/cookies.js', ['goog.net.Cookies', 'goog.net.cookies'], ['goog.userAgent']);
+goog.addDependency('net/channelrequest.js', ['goog.net.ChannelRequest', 'goog.net.ChannelRequest.Error'], ['goog.Timer', 'goog.async.Throttle', 'goog.events', 'goog.events.EventHandler', 'goog.net.EventType', 'goog.net.XmlHttp.ReadyState', 'goog.object', 'goog.userAgent']);
+goog.addDependency('net/cookies.js', ['goog.net.Cookies', 'goog.net.cookies'], []);
 goog.addDependency('net/crossdomainrpc.js', ['goog.net.CrossDomainRpc'], ['goog.Uri.QueryData', 'goog.debug.Logger', 'goog.dom', 'goog.events', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.json', 'goog.net.EventType', 'goog.net.HttpStatus', 'goog.userAgent']);
 goog.addDependency('net/errorcode.js', ['goog.net.ErrorCode'], []);
 goog.addDependency('net/eventtype.js', ['goog.net.EventType'], []);
 goog.addDependency('net/filedownloader.js', ['goog.net.FileDownloader', 'goog.net.FileDownloader.Error'], ['goog.Disposable', 'goog.asserts', 'goog.async.Deferred', 'goog.crypt.hash32', 'goog.debug.Error', 'goog.events.EventHandler', 'goog.fs', 'goog.fs.DirectoryEntry.Behavior', 'goog.fs.Error.ErrorCode', 'goog.fs.FileSaver.EventType', 'goog.net.EventType', 'goog.net.XhrIo.ResponseType', 'goog.net.XhrIoPool']);
 goog.addDependency('net/httpstatus.js', ['goog.net.HttpStatus'], []);
-goog.addDependency('net/iframeio.js', ['goog.net.IframeIo', 'goog.net.IframeIo.IncrementalDataEvent'], ['goog.Timer', 'goog.Uri', 'goog.debug', 'goog.debug.Logger', 'goog.dom', 'goog.events', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.json', 'goog.net.ErrorCode', 'goog.net.EventType', 'goog.reflect', 'goog.string', 'goog.structs', 'goog.userAgent']);
+goog.addDependency('net/iframeio.js', ['goog.net.IframeIo', 'goog.net.IframeIo.IncrementalDataEvent'], ['goog.Timer', 'goog.Uri', 'goog.debug', 'goog.debug.Logger', 'goog.dom', 'goog.events', 'goog.events.Event', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.json', 'goog.net.ErrorCode', 'goog.net.EventType', 'goog.reflect', 'goog.string', 'goog.structs', 'goog.userAgent']);
 goog.addDependency('net/iframeloadmonitor.js', ['goog.net.IframeLoadMonitor'], ['goog.dom', 'goog.events', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.userAgent']);
 goog.addDependency('net/imageloader.js', ['goog.net.ImageLoader'], ['goog.array', 'goog.dom', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.net.EventType', 'goog.object', 'goog.userAgent']);
 goog.addDependency('net/ipaddress.js', ['goog.net.IpAddress', 'goog.net.Ipv4Address', 'goog.net.Ipv6Address'], ['goog.array', 'goog.math.Integer', 'goog.object', 'goog.string']);
@@ -583,6 +592,7 @@
 goog.addDependency('testing/performancetable.js', ['goog.testing.PerformanceTable'], ['goog.dom', 'goog.testing.PerformanceTimer']);
 goog.addDependency('testing/performancetimer.js', ['goog.testing.PerformanceTimer', 'goog.testing.PerformanceTimer.Task'], ['goog.array', 'goog.math']);
 goog.addDependency('testing/propertyreplacer.js', ['goog.testing.PropertyReplacer'], ['goog.userAgent']);
+goog.addDependency('testing/proto2/proto2.js', ['goog.testing.proto2'], ['goog.proto2.Message', 'goog.testing.asserts']);
 goog.addDependency('testing/pseudorandom.js', ['goog.testing.PseudoRandom'], ['goog.Disposable']);
 goog.addDependency('testing/recordfunction.js', ['goog.testing.FunctionCall', 'goog.testing.recordConstructor', 'goog.testing.recordFunction'], []);
 goog.addDependency('testing/shardingtestcase.js', ['goog.testing.ShardingTestCase'], ['goog.asserts', 'goog.testing.TestCase']);
@@ -639,7 +649,7 @@
 goog.addDependency('ui/colorsplitbehavior.js', ['goog.ui.ColorSplitBehavior'], ['goog.ui.ColorButton', 'goog.ui.ColorMenuButton', 'goog.ui.SplitBehavior']);
 goog.addDependency('ui/combobox.js', ['goog.ui.ComboBox', 'goog.ui.ComboBoxItem'], ['goog.Timer', 'goog.debug.Logger', 'goog.dom.classes', 'goog.events', 'goog.events.InputHandler', 'goog.events.KeyCodes', 'goog.events.KeyHandler', 'goog.positioning.Corner', 'goog.positioning.MenuAnchoredPosition', 'goog.string', 'goog.style', 'goog.ui.Component', 'goog.ui.ItemEvent', 'goog.ui.LabelInput', 'goog.ui.Menu', 'goog.ui.MenuItem', 'goog.ui.registry', 'goog.userAgent']);
 goog.addDependency('ui/component.js', ['goog.ui.Component', 'goog.ui.Component.Error', 'goog.ui.Component.EventType', 'goog.ui.Component.State'], ['goog.array', 'goog.array.ArrayLike', 'goog.dom', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.object', 'goog.style', 'goog.ui.IdGenerator']);
-goog.addDependency('ui/container.js', ['goog.ui.Container', 'goog.ui.Container.EventType', 'goog.ui.Container.Orientation'], ['goog.dom', 'goog.dom.a11y', 'goog.dom.a11y.State', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.KeyHandler', 'goog.events.KeyHandler.EventType', 'goog.style', 'goog.ui.Component', 'goog.ui.Component.Error', 'goog.ui.Component.EventType', 'goog.ui.Component.State', 'goog.ui.ContainerRenderer']);
+goog.addDependency('ui/container.js', ['goog.ui.Container', 'goog.ui.Container.EventType', 'goog.ui.Container.Orientation'], ['goog.dom', 'goog.dom.a11y', 'goog.dom.a11y.State', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.KeyHandler', 'goog.events.KeyHandler.EventType', 'goog.style', 'goog.ui.Component', 'goog.ui.Component.Error', 'goog.ui.Component.EventType', 'goog.ui.Component.State', 'goog.ui.ContainerRenderer', 'goog.ui.Control']);
 goog.addDependency('ui/containerrenderer.js', ['goog.ui.ContainerRenderer'], ['goog.array', 'goog.dom', 'goog.dom.a11y', 'goog.dom.classes', 'goog.string', 'goog.style', 'goog.ui.Separator', 'goog.ui.registry', 'goog.userAgent']);
 goog.addDependency('ui/containerscroller.js', ['goog.ui.ContainerScroller'], ['goog.Timer', 'goog.events.EventHandler', 'goog.style', 'goog.ui.Component', 'goog.ui.Component.EventType', 'goog.ui.Container.EventType']);
 goog.addDependency('ui/control.js', ['goog.ui.Control'], ['goog.array', 'goog.dom', 'goog.events.BrowserEvent.MouseButton', 'goog.events.Event', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.KeyHandler', 'goog.events.KeyHandler.EventType', 'goog.string', 'goog.ui.Component', 'goog.ui.Component.Error', 'goog.ui.Component.EventType', 'goog.ui.Component.State', 'goog.ui.ControlContent', 'goog.ui.ControlRenderer', 'goog.ui.decorate', 'goog.ui.registry', 'goog.userAgent']);
@@ -743,7 +753,7 @@
 goog.addDependency('ui/offlinestatuscard.js', ['goog.ui.OfflineStatusCard', 'goog.ui.OfflineStatusCard.EventType'], ['goog.dom', 'goog.events.EventType', 'goog.gears.StatusType', 'goog.structs.Map', 'goog.style', 'goog.ui.Component', 'goog.ui.Component.EventType', 'goog.ui.ProgressBar']);
 goog.addDependency('ui/offlinestatuscomponent.js', ['goog.ui.OfflineStatusComponent', 'goog.ui.OfflineStatusComponent.StatusClassNames'], ['goog.dom.classes', 'goog.events.EventType', 'goog.gears.StatusType', 'goog.positioning', 'goog.positioning.AnchoredPosition', 'goog.positioning.Corner', 'goog.positioning.Overflow', 'goog.ui.Component', 'goog.ui.OfflineStatusCard.EventType', 'goog.ui.Popup']);
 goog.addDependency('ui/option.js', ['goog.ui.Option'], ['goog.ui.Component.EventType', 'goog.ui.ControlContent', 'goog.ui.MenuItem', 'goog.ui.registry']);
-goog.addDependency('ui/palette.js', ['goog.ui.Palette'], ['goog.array', 'goog.dom', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.math.Size', 'goog.ui.Component.Error', 'goog.ui.Component.EventType', 'goog.ui.Control', 'goog.ui.PaletteRenderer', 'goog.ui.SelectionModel']);
+goog.addDependency('ui/palette.js', ['goog.ui.Palette'], ['goog.array', 'goog.dom', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.math.Size', 'goog.ui.Component', 'goog.ui.Control', 'goog.ui.PaletteRenderer', 'goog.ui.SelectionModel']);
 goog.addDependency('ui/paletterenderer.js', ['goog.ui.PaletteRenderer'], ['goog.array', 'goog.dom', 'goog.dom.NodeType', 'goog.dom.a11y', 'goog.dom.classes', 'goog.style', 'goog.ui.ControlRenderer', 'goog.userAgent']);
 goog.addDependency('ui/plaintextspellchecker.js', ['goog.ui.PlainTextSpellChecker'], ['goog.Timer', 'goog.dom', 'goog.dom.a11y', 'goog.events.EventHandler', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.events.KeyHandler', 'goog.events.KeyHandler.EventType', 'goog.style', 'goog.ui.AbstractSpellChecker', 'goog.ui.AbstractSpellChecker.AsyncResult', 'goog.ui.Component.EventType', 'goog.userAgent']);
 goog.addDependency('ui/popup.js', ['goog.ui.Popup', 'goog.ui.Popup.AbsolutePosition', 'goog.ui.Popup.AnchoredPosition', 'goog.ui.Popup.AnchoredViewPortPosition', 'goog.ui.Popup.ClientPosition', 'goog.ui.Popup.Corner', 'goog.ui.Popup.Overflow', 'goog.ui.Popup.ViewPortClientPosition', 'goog.ui.Popup.ViewPortPosition'], ['goog.math.Box', 'goog.positioning', 'goog.positioning.AbsolutePosition', 'goog.positioning.AnchoredPosition', 'goog.positioning.AnchoredViewportPosition', 'goog.positioning.ClientPosition', 'goog.positioning.Corner', 'goog.positioning.Overflow', 'goog.positioning.OverflowStatus', 'goog.positioning.ViewportClientPosition', 'goog.positioning.ViewportPosition', 'goog.style', 'goog.ui.PopupBase']);
diff --git a/third_party/closure/goog/disposable/disposable.js b/third_party/closure/goog/disposable/disposable.js
index 3f99e16..8e10083 100644
--- a/third_party/closure/goog/disposable/disposable.js
+++ b/third_party/closure/goog/disposable/disposable.js
@@ -203,8 +203,9 @@
 /**
  * Invokes a callback function when this object is disposed. Callbacks are
  * invoked in the order in which they were added.
- * @param {!Function} callback The callback function.
- * @param {Object=} opt_scope An optional scope to call the callback in.
+ * @param {function(this:T):?} callback The callback function.
+ * @param {T=} opt_scope An optional scope to call the callback in.
+ * @template T
  */
 goog.Disposable.prototype.addOnDisposeCallback = function(callback, opt_scope) {
   if (!this.onDisposeCallbacks_) {
diff --git a/third_party/closure/goog/dom/bufferedviewportsizemonitor.js b/third_party/closure/goog/dom/bufferedviewportsizemonitor.js
new file mode 100644
index 0000000..a4e32a5
--- /dev/null
+++ b/third_party/closure/goog/dom/bufferedviewportsizemonitor.js
@@ -0,0 +1,202 @@
+// Copyright 2012 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview A viewport size monitor that buffers RESIZE events until the
+ * window size has stopped changing, within a specified period of time.  For
+ * every RESIZE event dispatched, this will dispatch up to two *additional*
+ * events:
+ * - {@link #EventType.RESIZE_WIDTH} if the viewport's width has changed since
+ *   the last buffered dispatch.
+ * - {@link #EventType.RESIZE_HEIGHT} if the viewport's height has changed since
+ *   the last buffered dispatch.
+ * You likely only need to listen to one of the three events.  But if you need
+ * more, just be cautious of duplicating effort.
+ *
+ */
+
+goog.provide('goog.dom.BufferedViewportSizeMonitor');
+
+goog.require('goog.asserts');
+goog.require('goog.async.Delay');
+goog.require('goog.events');
+goog.require('goog.events.EventTarget');
+goog.require('goog.events.EventType');
+
+
+
+/**
+ * Creates a new BufferedViewportSizeMonitor.
+ * @param {!goog.dom.ViewportSizeMonitor} viewportSizeMonitor The
+ *     underlying viewport size monitor.
+ * @param {number=} opt_bufferMs The buffer time, in ms. If not specified, this
+ *     value defaults to {@link #RESIZE_EVENT_DELAY_MS_}.
+ * @constructor
+ * @extends {goog.events.EventTarget}
+ */
+goog.dom.BufferedViewportSizeMonitor = function(
+    viewportSizeMonitor, opt_bufferMs) {
+  goog.base(this);
+
+  /**
+   * The underlying viewport size monitor.
+   * @type {goog.dom.ViewportSizeMonitor}
+   * @private
+   */
+  this.viewportSizeMonitor_ = viewportSizeMonitor;
+
+  /**
+   * The current size of the viewport.
+   * @type {goog.math.Size}
+   * @private
+   */
+  this.currentSize_ = this.viewportSizeMonitor_.getSize();
+
+  /**
+   * The resize buffer time in ms.
+   * @type {number}
+   * @private
+   */
+  this.resizeBufferMs_ = opt_bufferMs ||
+      goog.dom.BufferedViewportSizeMonitor.RESIZE_EVENT_DELAY_MS_;
+
+  /**
+   * Listener key for the viewport size monitor.
+   * @type {number}
+   * @private
+   */
+  this.listenerKey_ = /** @type {number} */ goog.events.listen(
+      viewportSizeMonitor,
+      goog.events.EventType.RESIZE,
+      this.handleResize_,
+      false,
+      this);
+};
+goog.inherits(goog.dom.BufferedViewportSizeMonitor, goog.events.EventTarget);
+
+
+/**
+ * Additional events to dispatch.
+ * @enum {string}
+ */
+goog.dom.BufferedViewportSizeMonitor.EventType = {
+  RESIZE_HEIGHT: goog.events.getUniqueId('resizeheight'),
+  RESIZE_WIDTH: goog.events.getUniqueId('resizewidth')
+};
+
+
+/**
+ * Delay for the resize event.
+ * @type {goog.async.Delay}
+ * @private
+ */
+goog.dom.BufferedViewportSizeMonitor.prototype.resizeDelay_;
+
+
+/**
+ * Default number of milliseconds to wait after a resize event to relayout the
+ * page.
+ * @type {number}
+ * @const
+ * @private
+ */
+goog.dom.BufferedViewportSizeMonitor.RESIZE_EVENT_DELAY_MS_ = 100;
+
+
+/** @override */
+goog.dom.BufferedViewportSizeMonitor.prototype.disposeInternal =
+    function() {
+  goog.events.unlistenByKey(this.listenerKey_);
+  goog.base(this, 'disposeInternal');
+};
+
+
+/**
+ * Handles resize events on the underlying ViewportMonitor.
+ * @private
+ */
+goog.dom.BufferedViewportSizeMonitor.prototype.handleResize_ =
+    function() {
+  // Lazily create when needed.
+  if (!this.resizeDelay_) {
+    this.resizeDelay_ = new goog.async.Delay(
+        this.onWindowResize_,
+        this.resizeBufferMs_,
+        this);
+    this.registerDisposable(this.resizeDelay_);
+  }
+  this.resizeDelay_.start();
+};
+
+
+/**
+ * Window resize callback that determines whether to reflow the view contents.
+ * @private
+ */
+goog.dom.BufferedViewportSizeMonitor.prototype.onWindowResize_ =
+    function() {
+  if (this.viewportSizeMonitor_.isDisposed()) {
+    return;
+  }
+
+  var previousSize = this.currentSize_;
+  var currentSize = this.viewportSizeMonitor_.getSize();
+
+  goog.asserts.assert(currentSize,
+      'Viewport size should be set at this point');
+
+  this.currentSize_ = currentSize;
+
+  if (previousSize) {
+
+    var resized = false;
+
+    // Width has changed
+    if (previousSize.width != currentSize.width) {
+      this.dispatchEvent(
+          goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_WIDTH);
+      resized = true;
+    }
+
+    // Height has changed
+    if (previousSize.height != currentSize.height) {
+      this.dispatchEvent(
+          goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_HEIGHT);
+      resized = true;
+    }
+
+    // If either has changed, this is a resize event.
+    if (resized) {
+      this.dispatchEvent(goog.events.EventType.RESIZE);
+    }
+
+  } else {
+    // If we didn't have a previous size, we consider all events to have
+    // changed.
+    this.dispatchEvent(
+        goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_HEIGHT);
+    this.dispatchEvent(
+        goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_WIDTH);
+    this.dispatchEvent(goog.events.EventType.RESIZE);
+  }
+};
+
+
+/**
+ * Returns the current size of the viewport.
+ * @return {goog.math.Size?} The current viewport size.
+ */
+goog.dom.BufferedViewportSizeMonitor.prototype.getSize = function() {
+  return this.currentSize_ ? this.currentSize_.clone() : null;
+};
diff --git a/third_party/closure/goog/dom/dom.js b/third_party/closure/goog/dom/dom.js
index 2a179a5..164a93d 100644
--- a/third_party/closure/goog/dom/dom.js
+++ b/third_party/closure/goog/dom/dom.js
@@ -1290,9 +1290,7 @@
   }
 
   // Special case for document nodes on IE 7 and 8.
-  if ((node1.nodeType == goog.dom.NodeType.DOCUMENT ||
-      node2.nodeType == goog.dom.NodeType.DOCUMENT) &&
-      goog.userAgent.IE && !goog.userAgent.isVersion(9)) {
+  if (goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) {
     if (node1.nodeType == goog.dom.NodeType.DOCUMENT) {
       return -1;
     }
diff --git a/third_party/closure/goog/editor/browserfeature.js b/third_party/closure/goog/editor/browserfeature.js
index 678dd04..5e03eca 100644
--- a/third_party/closure/goog/editor/browserfeature.js
+++ b/third_party/closure/goog/editor/browserfeature.js
@@ -147,7 +147,8 @@
       goog.userAgent.WEBKIT || goog.userAgent.OPERA,
 
   // Whether clicking on an editable link will take you to that site.
-  FOLLOWS_EDITABLE_LINKS: goog.userAgent.WEBKIT,
+  FOLLOWS_EDITABLE_LINKS: goog.userAgent.WEBKIT ||
+      goog.userAgent.IE && goog.userAgent.isVersion('9'),
 
   // Whether this browser has document.activeElement available.
   HAS_ACTIVE_ELEMENT:
diff --git a/third_party/closure/goog/editor/field.js b/third_party/closure/goog/editor/field.js
index cd66ea2..0a4f8bc 100644
--- a/third_party/closure/goog/editor/field.js
+++ b/third_party/closure/goog/editor/field.js
@@ -26,12 +26,12 @@
 goog.provide('goog.editor.Field.EventType');
 
 goog.require('goog.array');
+goog.require('goog.asserts');
 goog.require('goog.async.Delay');
 goog.require('goog.debug.Logger');
 goog.require('goog.dom');
 goog.require('goog.dom.Range');
 goog.require('goog.dom.TagName');
-goog.require('goog.dom.classes');
 goog.require('goog.editor.BrowserFeature');
 goog.require('goog.editor.Command');
 goog.require('goog.editor.Plugin');
@@ -334,6 +334,22 @@
 
 
 /**
+ * Flag controlling wether to capture mouse up events on the window or not.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.Field.prototype.useWindowMouseUp_ = false;
+
+
+/**
+ * FLag indicating the handling of a mouse event sequence.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.Field.prototype.waitingForMouseUp_ = false;
+
+
+/**
  * Sets the active field id.
  * @param {?string} fieldId The active field id.
  */
@@ -351,6 +367,18 @@
 
 
 /**
+ * Sets flag to control whether to use window mouse up after seeing
+ * a mouse down operation on the field.
+ * @param {boolean} flag True to track window mouse up.
+ */
+goog.editor.Field.prototype.setUseWindowMouseUp = function(flag) {
+  goog.asserts.assert(!flag || !this.usesIframe(),
+      'procssing window mouse up should only be enabled when not using iframe');
+  this.useWindowMouseUp_ = flag;
+};
+
+
+/**
  * @return {boolean} Whether we're in modal interaction mode. When this
  *     returns true, another plugin is interacting with the field contents
  *     in a synchronous way, and expects you not to make changes to
@@ -840,7 +868,13 @@
   }
 
   this.addListener(goog.events.EventType.MOUSEDOWN, this.handleMouseDown_);
-  this.addListener(goog.events.EventType.MOUSEUP, this.handleMouseUp_);
+  if (this.useWindowMouseUp_) {
+    this.eventRegister.listen(this.editableDomHelper.getDocument(),
+        goog.events.EventType.MOUSEUP, this.handleMouseUp_);
+    this.addListener(goog.events.EventType.DRAGSTART, this.handleDragStart_);
+  } else {
+    this.addListener(goog.events.EventType.MOUSEUP, this.handleMouseUp_);
+  }
 };
 
 
@@ -1927,11 +1961,7 @@
  * @private
  */
 goog.editor.Field.prototype.handleMouseDown_ = function(e) {
-  // If the user clicks on an object (like an image) in the field
-  // and the activeField is not set, set it.
-  if (!goog.editor.Field.getActiveFieldId()) {
-    goog.editor.Field.setActiveFieldId(this.id);
-  }
+  goog.editor.Field.setActiveFieldId(this.id);
 
   // Open links in a new window if the user control + clicks.
   if (goog.userAgent.IE) {
@@ -1941,6 +1971,18 @@
       this.originalDomHelper.getWindow().open(targetElement.href);
     }
   }
+  this.waitingForMouseUp_ = true;
+};
+
+
+/**
+ * Handle drag start. Needs to cancel listening for the mouse up event on the
+ * window.
+ * @param {goog.events.BrowserEvent} e The event.
+ * @private
+ */
+goog.editor.Field.prototype.handleDragStart_ = function(e) {
+  this.waitingForMouseUp_ = false;
 };
 
 
@@ -1950,6 +1992,11 @@
  * @private
  */
 goog.editor.Field.prototype.handleMouseUp_ = function(e) {
+  if (this.useWindowMouseUp_ && !this.waitingForMouseUp_) {
+    return;
+  }
+  this.waitingForMouseUp_ = false;
+
   /*
    * We fire a selection change event immediately for listeners that depend on
    * the native browser event object (e).  On IE, a listener that tries to
diff --git a/third_party/closure/goog/editor/link.js b/third_party/closure/goog/editor/link.js
index 6d89a64..0a50665 100644
--- a/third_party/closure/goog/editor/link.js
+++ b/third_party/closure/goog/editor/link.js
@@ -214,20 +214,33 @@
 
 
 /**
+ * @return {string?} The modified string for the link if the link
+ *     text appears to be a valid link. Returns null if this is not
+ *     a valid link address.
+ */
+goog.editor.Link.prototype.getValidLinkFromText = function() {
+  var text = this.getCurrentText();
+  if (goog.editor.Link.isLikelyUrl(text)) {
+    if (text.search(/:/) < 0) {
+      return 'http://' + goog.string.trimLeft(text);
+    }
+    return text;
+  } else if (goog.editor.Link.isLikelyEmailAddress(text)) {
+    return 'mailto:' + text;
+  }
+  return null;
+};
+
+
+/**
  * After link creation, finish creating the link depending on the type
  * of link being created.
  * @param {goog.editor.Field} field The field where this link is being created.
  */
 goog.editor.Link.prototype.finishLinkCreation = function(field) {
-  var text = this.getCurrentText();
-  if (goog.editor.Link.isLikelyUrl(text)) {
-    if (text.search(/:/) < 0) {
-      text = 'http://' + goog.string.trimLeft(text);
-    }
-    this.updateLinkDisplay_(field, text);
-  } else if (goog.editor.Link.isLikelyEmailAddress(text)) {
-    text = 'mailto:' + text;
-    this.updateLinkDisplay_(field, text);
+  var linkFromText = this.getValidLinkFromText();
+  if (linkFromText) {
+    this.updateLinkDisplay_(field, linkFromText);
   } else {
     field.execCommand(goog.editor.Command.MODAL_LINK_EDITOR, this);
   }
diff --git a/third_party/closure/goog/editor/plugins/abstractbubbleplugin.js b/third_party/closure/goog/editor/plugins/abstractbubbleplugin.js
index d02556d..cc230ee 100644
--- a/third_party/closure/goog/editor/plugins/abstractbubbleplugin.js
+++ b/third_party/closure/goog/editor/plugins/abstractbubbleplugin.js
@@ -27,6 +27,8 @@
 goog.require('goog.events');
 goog.require('goog.events.EventHandler');
 goog.require('goog.events.EventType');
+goog.require('goog.events.KeyCodes');
+goog.require('goog.events.actionEventWrapper');
 goog.require('goog.functions');
 goog.require('goog.string.Unicode');
 goog.require('goog.ui.Component.EventType');
@@ -153,6 +155,27 @@
 
 
 /**
+ * Whether this bubble should support tabbing through the link elements. False
+ * by default.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ =
+    false;
+
+
+/**
+ * Sets whether the bubble should support tabbing through the link elements.
+ * @param {boolean} keyboardNavigationEnabled Whether the bubble should support
+ *     tabbing through the link elements.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.enableKeyboardNavigation =
+    function(keyboardNavigationEnabled) {
+  this.keyboardNavigationEnabled_ = keyboardNavigationEnabled;
+};
+
+
+/**
  * Sets the bubble parent.
  * @param {Element} bubbleParent An element where the bubble will be
  *     anchored. If null, we will use the application document. This
@@ -338,7 +361,13 @@
         this.shouldPreferBubbleAboveElement());
     this.eventRegister.listen(bubble, goog.ui.Component.EventType.HIDE,
         this.handlePanelClosed_);
+
     this.onShow();
+
+    if (this.keyboardNavigationEnabled_) {
+      this.eventRegister.listen(bubble.getContentElement(),
+          goog.events.EventType.KEYDOWN, this.onBubbleKey_);
+    }
   }
 };
 
@@ -388,10 +417,25 @@
  * @param {Element} target The event source element.
  * @param {Function} handler The event handler.
  * @protected
+ * @deprecated Use goog.editor.plugins.AbstractBubblePlugin.
+ *     registerActionHandler to register click and enter events.
  */
 goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler =
     function(target, handler) {
-  this.eventRegister.listen(target, goog.events.EventType.CLICK, handler);
+  this.registerActionHandler(target, handler);
+};
+
+
+/**
+ * Register the handler for the target's CLICK and ENTER key events.
+ * @param {Element} target The event source element.
+ * @param {Function} handler The event handler.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.registerActionHandler =
+    function(target, handler) {
+  this.eventRegister.listenWithWrapper(target, goog.events.actionEventWrapper,
+      handler);
 };
 
 
@@ -428,6 +472,53 @@
 
 
 /**
+ * In case the keyboard navigation is enabled, this will focus to the first link
+ * element in the bubble when TAB is clicked. The user could still go through
+ * the rest of tabbable UI elements using shift + TAB.
+ * @override
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyDown = function(e) {
+  if (this.keyboardNavigationEnabled_ &&
+      this.isVisible() &&
+      e.keyCode == goog.events.KeyCodes.TAB && !e.shiftKey) {
+    var bubbleEl = this.getSharedBubble_().getContentElement();
+    var linkEl = goog.dom.getElementByClass(
+        goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_, bubbleEl);
+    if (linkEl) {
+      linkEl.focus();
+      e.preventDefault();
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Handles a key event on the bubble. This ensures that the focus loops through
+ * the link elements found in the bubble and then the focus is got by the field
+ * element.
+ * @param {goog.events.BrowserEvent} e The event.
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.onBubbleKey_ = function(e) {
+  if (this.isVisible() &&
+      e.keyCode == goog.events.KeyCodes.TAB) {
+    var bubbleEl = this.getSharedBubble_().getContentElement();
+    var links = goog.dom.getElementsByClass(
+        goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_, bubbleEl);
+    var tabbingOutOfBubble = e.shiftKey ?
+        links[0] == e.target :
+        links.length && links[links.length - 1] == e.target;
+    if (tabbingOutOfBubble) {
+      this.getFieldObject().focus();
+      e.preventDefault();
+    }
+  }
+};
+
+
+/**
  * @return {boolean} Whether the bubble is visible.
  */
 goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() {
@@ -482,7 +573,7 @@
     linkId, linkText, opt_onClick, opt_container) {
   var link = this.createLinkHelper(linkId, linkText, false, opt_container);
   if (opt_onClick) {
-    this.registerClickHandler(link, opt_onClick);
+    this.registerActionHandler(link, opt_onClick);
   }
   return link;
 };
@@ -506,6 +597,10 @@
       isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN,
       {className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_},
       linkText);
+  if (this.keyboardNavigationEnabled_) {
+    link.setAttribute('tabindex', 0);
+  }
+  link.setAttribute('role', 'link');
   this.setupLink(link, linkId, opt_container);
   goog.editor.style.makeUnselectable(link, this.eventRegister);
   return link;
diff --git a/third_party/closure/goog/editor/plugins/linkbubble.js b/third_party/closure/goog/editor/plugins/linkbubble.js
index 0707357..f761f6c 100644
--- a/third_party/closure/goog/editor/plugins/linkbubble.js
+++ b/third_party/closure/goog/editor/plugins/linkbubble.js
@@ -439,10 +439,15 @@
 
 
 /**
- * Shows the link dialog
+ * Shows the link dialog.
+ * @param {goog.events.BrowserEvent} e The event.
  * @private
  */
-goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function() {
+goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {
+  // Needed when this occurs due to an ENTER key event, else the newly created
+  // dialog manages to have its OK button pressed, causing it to disappear.
+  e.preventDefault();
+
   this.getFieldObject().execCommand(goog.editor.Command.MODAL_LINK_EDITOR,
       new goog.editor.Link(
           /** @type {HTMLAnchorElement} */ (this.getTargetElement()),
@@ -466,6 +471,7 @@
   this.closeBubble();
 
   this.getFieldObject().dispatchChange();
+  this.getFieldObject().focus();
 };
 
 
diff --git a/third_party/closure/goog/editor/plugins/linkdialogplugin.js b/third_party/closure/goog/editor/plugins/linkdialogplugin.js
index 5776493..eb525ad 100644
--- a/third_party/closure/goog/editor/plugins/linkdialogplugin.js
+++ b/third_party/closure/goog/editor/plugins/linkdialogplugin.js
@@ -292,7 +292,7 @@
   dialog.setStopReferrerLeaks(this.stopReferrerLeaks_);
   this.eventHandler_.
       listen(dialog, goog.ui.editor.AbstractDialog.EventType.OK,
-          this.handleOk_).
+          this.handleOk).
       listen(dialog, goog.ui.editor.AbstractDialog.EventType.CANCEL,
           this.handleCancel_).
       listen(dialog, goog.ui.editor.LinkDialog.EventType.BEFORE_TEST_LINK,
@@ -311,9 +311,9 @@
 /**
  * Handles the OK event from the dialog by updating the link in the field.
  * @param {goog.ui.editor.LinkDialog.OkEvent} e OK event object.
- * @private
+ * @protected
  */
-goog.editor.plugins.LinkDialogPlugin.prototype.handleOk_ = function(e) {
+goog.editor.plugins.LinkDialogPlugin.prototype.handleOk = function(e) {
   // We're not restoring the original selection, so clear it out.
   this.disposeOriginalSelection();
 
diff --git a/third_party/closure/goog/editor/plugins/linkshortcutplugin.js b/third_party/closure/goog/editor/plugins/linkshortcutplugin.js
index 1f1ac69..c7f22a1 100644
--- a/third_party/closure/goog/editor/plugins/linkshortcutplugin.js
+++ b/third_party/closure/goog/editor/plugins/linkshortcutplugin.js
@@ -49,7 +49,7 @@
 goog.editor.plugins.LinkShortcutPlugin.prototype.handleKeyboardShortcut =
     function(e, key, isModifierPressed) {
   var command;
-  if (isModifierPressed && key == 'k') {
+  if (isModifierPressed && key == 'k' && !e.shiftKey) {
     var link = /** @type {goog.editor.Link?} */ (
         this.getFieldObject().execCommand(goog.editor.Command.LINK));
     if (link) {
diff --git a/third_party/closure/goog/events/browserfeature.js b/third_party/closure/goog/events/browserfeature.js
index 0cc103f..f5b28fd 100644
--- a/third_party/closure/goog/events/browserfeature.js
+++ b/third_party/closure/goog/events/browserfeature.js
@@ -67,5 +67,17 @@
    */
   HTML5_NETWORK_EVENTS_FIRE_ON_BODY:
       goog.userAgent.GECKO && !goog.userAgent.isVersion('8') ||
-      goog.userAgent.IE && !goog.userAgent.isVersion('9')
+      goog.userAgent.IE && !goog.userAgent.isVersion('9'),
+
+  /**
+   * Whether touch is enabled in the browser.
+   */
+  TOUCH_ENABLED:
+      ('ontouchstart' in goog.global ||
+          !!(goog.global['document'] &&
+             document.documentElement &&
+             'ontouchstart' in document.documentElement) ||
+          // IE10 uses non-standard touch events, so it has a different check.
+          !!(goog.global['navigator'] &&
+              goog.global['navigator']['msMaxTouchPoints']))
 };
diff --git a/third_party/closure/goog/events/eventtype.js b/third_party/closure/goog/events/eventtype.js
index eaac23c..f99a224 100644
--- a/third_party/closure/goog/events/eventtype.js
+++ b/third_party/closure/goog/events/eventtype.js
@@ -69,10 +69,12 @@
 
   // Drag and drop
   DRAGSTART: 'dragstart',
+  DRAG: 'drag',
   DRAGENTER: 'dragenter',
   DRAGOVER: 'dragover',
   DRAGLEAVE: 'dragleave',
   DROP: 'drop',
+  DRAGEND: 'dragend',
 
   // WebKit touch events.
   TOUCHSTART: 'touchstart',
@@ -81,6 +83,7 @@
   TOUCHCANCEL: 'touchcancel',
 
   // Misc
+  BEFOREUNLOAD: 'beforeunload',
   CONTEXTMENU: 'contextmenu',
   ERROR: 'error',
   HELP: 'help',
@@ -121,5 +124,20 @@
   // CSS transition events. Based on the browser support described at:
   // https://developer.mozilla.org/en/css/css_transitions#Browser_compatibility
   TRANSITIONEND: goog.userAgent.WEBKIT ? 'webkitTransitionEnd' :
-      (goog.userAgent.OPERA ? 'oTransitionEnd' : 'transitionend')
+      (goog.userAgent.OPERA ? 'oTransitionEnd' : 'transitionend'),
+
+  // IE specific events.
+  // See http://msdn.microsoft.com/en-us/library/ie/hh673557(v=vs.85).aspx
+  MSGESTURECHANGE: 'MSGestureChange',
+  MSGESTUREEND: 'MSGestureEnd',
+  MSGESTUREHOLD: 'MSGestureHold',
+  MSGESTURESTART: 'MSGestureStart',
+  MSGESTURETAP: 'MSGestureTap',
+  MSINERTIASTART: 'MSInertiaStart',
+  MSPOINTERCANCEL: 'MSPointerCancel',
+  MSPOINTERDOWN: 'MSPointerDown',
+  MSPOINTERMOVE: 'MSPointerMove',
+  MSPOINTEROVER: 'MSPointerOver',
+  MSPOINTEROUT: 'MSPointerOut',
+  MSPOINTERUP: 'MSPointerUp'
 };
diff --git a/third_party/closure/goog/events/inputhandler.js b/third_party/closure/goog/events/inputhandler.js
index 7c3c9ed..4375339 100644
--- a/third_party/closure/goog/events/inputhandler.js
+++ b/third_party/closure/goog/events/inputhandler.js
@@ -58,7 +58,7 @@
  * @extends {goog.events.EventTarget}
  */
 goog.events.InputHandler = function(element) {
-  goog.events.EventTarget.call(this);
+  goog.base(this);
 
   /**
    * The element that you want to listen for input events on.
@@ -67,20 +67,16 @@
    */
   this.element_ = element;
 
-  /**
-   * Whether input event is emulated.
-   * IE doesn't support input events. We could use property change events but
-   * they are broken in many ways:
-   * - Fire even if value was changed programmatically.
-   * - Aren't always delivered. For example, if you change value or even width
-   *   of input programmatically, next value change made by user won't fire an
-   *   event.
-   * WebKit before version 531 did not support input events for textareas.
-   * @type {boolean}
-   * @private
-   */
-  this.inputEventEmulation_ =
-      goog.userAgent.IE ||
+  // Determine whether input event should be emulated.
+  // IE8 doesn't support input events. We could use property change events but
+  // they are broken in many ways:
+  // - Fire even if value was changed programmatically.
+  // - Aren't always delivered. For example, if you change value or even width
+  //   of input programmatically, next value change made by user won't fire an
+  //   event.
+  // IE9 supports input events when characters are inserted, but not deleted.
+  // WebKit before version 531 did not support input events for textareas.
+  var emulateInputEvents = goog.userAgent.IE ||
       (goog.userAgent.WEBKIT && !goog.userAgent.isVersion('531') &&
           element.tagName == 'TEXTAREA');
 
@@ -89,9 +85,19 @@
    * @private
    */
   this.eventHandler_ = new goog.events.EventHandler(this);
+
+  // Even if input event emulation is enabled, still listen for input events
+  // since they may be partially supported by the browser (such as IE9).
+  // If the input event does fire, we will be able to dispatch synchronously.
+  // (InputHandler events being asynchronous for IE is a common issue for
+  // cases like auto-grow textareas where they result in a quick flash of
+  // scrollbars between the textarea content growing and it being resized to
+  // fit.)
   this.eventHandler_.listen(
       this.element_,
-      this.inputEventEmulation_ ? ['keydown', 'paste', 'cut', 'drop'] : 'input',
+      emulateInputEvents ?
+          ['keydown', 'paste', 'cut', 'drop', 'input'] :
+          'input',
       this);
 };
 goog.inherits(goog.events.InputHandler, goog.events.EventTarget);
@@ -119,7 +125,21 @@
  * @param {goog.events.BrowserEvent} e The underlying browser event.
  */
 goog.events.InputHandler.prototype.handleEvent = function(e) {
-  if (this.inputEventEmulation_) {
+  if (e.type == 'input') {
+    // This event happens after all the other events we listen to, so cancel
+    // an asynchronous event dispatch if we have it queued up.  Otherwise, we
+    // will end up firing an extra event.
+    this.cancelTimerIfSet_();
+
+    // Unlike other browsers, Opera fires an extra input event when an element
+    // is blurred after the user has input into it. Since Opera doesn't fire
+    // input event on drop, it's enough to check whether element still has focus
+    // to suppress bogus notification.
+    if (!goog.userAgent.OPERA || this.element_ ==
+        goog.dom.getOwnerDocument(this.element_).activeElement) {
+      this.dispatchEvent(this.createInputEvent_(e));
+    }
+  } else {
     // Filter out key events that don't modify text.
     if (e.type == 'keydown' &&
         !goog.events.KeyCodes.isTextModifyingKeyEvent(e)) {
@@ -156,15 +176,6 @@
         this.dispatchEvent(inputEvent);
       }
     }, 0, this);
-  } else {
-    // Unlike other browsers, Opera fires an extra input event when an element
-    // is blurred after the user has input into it. Since Opera doesn't fire
-    // input event on drop, it's enough to check whether element still has focus
-    // to suppress bogus notification.
-    if (!goog.userAgent.OPERA || this.element_ ==
-        goog.dom.getOwnerDocument(this.element_).activeElement) {
-      this.dispatchEvent(this.createInputEvent_(e));
-    }
   }
 };
 
@@ -196,7 +207,7 @@
 
 /** @override */
 goog.events.InputHandler.prototype.disposeInternal = function() {
-  goog.events.InputHandler.superClass_.disposeInternal.call(this);
+  goog.base(this, 'disposeInternal');
   this.eventHandler_.dispose();
   this.cancelTimerIfSet_();
   delete this.element_;
diff --git a/third_party/closure/goog/events/keycodes.js b/third_party/closure/goog/events/keycodes.js
index cfb3da6..9f17c87 100644
--- a/third_party/closure/goog/events/keycodes.js
+++ b/third_party/closure/goog/events/keycodes.js
@@ -258,10 +258,31 @@
   // check the user agent.
   if (!opt_shiftKey &&
       (opt_heldKeyCode == goog.events.KeyCodes.CTRL ||
-       opt_heldKeyCode == goog.events.KeyCodes.ALT)) {
+       opt_heldKeyCode == goog.events.KeyCodes.ALT ||
+       goog.userAgent.MAC &&
+       opt_heldKeyCode == goog.events.KeyCodes.META)) {
     return false;
   }
 
+  // Some keys with Ctrl/Shift do not issue keypress in WEBKIT.
+  if (goog.userAgent.WEBKIT && opt_ctrlKey && opt_shiftKey) {
+    switch (keyCode) {
+      case goog.events.KeyCodes.BACKSLASH:
+      case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
+      case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
+      case goog.events.KeyCodes.TILDE:
+      case goog.events.KeyCodes.SEMICOLON:
+      case goog.events.KeyCodes.DASH:
+      case goog.events.KeyCodes.EQUALS:
+      case goog.events.KeyCodes.COMMA:
+      case goog.events.KeyCodes.PERIOD:
+      case goog.events.KeyCodes.SLASH:
+      case goog.events.KeyCodes.APOSTROPHE:
+      case goog.events.KeyCodes.SINGLE_QUOTE:
+        return false;
+    }
+  }
+
   // When Ctrl+<somekey> is held in IE, it only fires a keypress once, but it
   // continues to fire keydown events as the event repeats.
   if (goog.userAgent.IE && opt_ctrlKey && opt_heldKeyCode == keyCode) {
diff --git a/third_party/closure/goog/events/keyhandler.js b/third_party/closure/goog/events/keyhandler.js
index 68d65f9..e6f5e58 100644
--- a/third_party/closure/goog/events/keyhandler.js
+++ b/third_party/closure/goog/events/keyhandler.js
@@ -301,15 +301,28 @@
  * @private
  */
 goog.events.KeyHandler.prototype.handleKeyDown_ = function(e) {
-
   // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window
   // before we've caught a key-up event.  If the last-key was one of these we
   // reset the state.
-  if (goog.userAgent.WEBKIT &&
-      (this.lastKey_ == goog.events.KeyCodes.CTRL && !e.ctrlKey ||
-       this.lastKey_ == goog.events.KeyCodes.ALT && !e.altKey)) {
-    this.lastKey_ = -1;
-    this.keyCode_ = -1;
+
+  if (goog.userAgent.WEBKIT) {
+    if (this.lastKey_ == goog.events.KeyCodes.CTRL && !e.ctrlKey ||
+        this.lastKey_ == goog.events.KeyCodes.ALT && !e.altKey ||
+        goog.userAgent.MAC &&
+        this.lastKey_ == goog.events.KeyCodes.META && !e.metaKey) {
+      this.lastKey_ = -1;
+      this.keyCode_ = -1;
+    }
+  }
+
+  if (this.lastKey_ == -1) {
+    if (e.ctrlKey && e.keyCode != goog.events.KeyCodes.CTRL) {
+      this.lastKey_ = goog.events.KeyCodes.CTRL;
+    } else if (e.altKey && e.keyCode != goog.events.KeyCodes.ALT) {
+      this.lastKey_ = goog.events.KeyCodes.ALT;
+    } else if (e.metaKey && e.keyCode != goog.events.KeyCodes.META) {
+      this.lastKey_ = goog.events.KeyCodes.META;
+    }
   }
 
   if (goog.events.KeyHandler.USES_KEYDOWN_ &&
@@ -328,6 +341,17 @@
 
 
 /**
+ * Resets the stored previous values. Needed to be called for webkit which will
+ * not generate a key up for meta key operations. This should only be called
+ * when having finished with repeat key possiblities.
+ */
+goog.events.KeyHandler.prototype.resetState = function() {
+  this.lastKey_ = -1;
+  this.keyCode_ = -1;
+};
+
+
+/**
  * Clears the stored previous key value, resetting the key repeat status. Uses
  * -1 because the Safari 3 Windows beta reports 0 for certain keys (like Home
  * and End.)
@@ -335,8 +359,7 @@
  * @private
  */
 goog.events.KeyHandler.prototype.handleKeyup_ = function(e) {
-  this.lastKey_ = -1;
-  this.keyCode_ = -1;
+  this.resetState();
   this.altKey_ = e.altKey;
 };
 
diff --git a/third_party/closure/goog/fs/fs.js b/third_party/closure/goog/fs/fs.js
index ceb324b..5873fe6 100644
--- a/third_party/closure/goog/fs/fs.js
+++ b/third_party/closure/goog/fs/fs.js
@@ -25,6 +25,7 @@
 
 goog.provide('goog.fs');
 
+goog.require('goog.array');
 goog.require('goog.async.Deferred');
 goog.require('goog.events');
 goog.require('goog.fs.Error');
@@ -168,11 +169,16 @@
  */
 goog.fs.getBlob = function(var_args) {
   var BlobBuilder = goog.global.BlobBuilder || goog.global.WebKitBlobBuilder;
-  var bb = new BlobBuilder();
-  for (var i = 0; i < arguments.length; i++) {
-    bb.append(arguments[i]);
+
+  if (goog.isDef(BlobBuilder)) {
+    var bb = new BlobBuilder();
+    for (var i = 0; i < arguments.length; i++) {
+      bb.append(arguments[i]);
+    }
+    return bb.getBlob();
+  } else {
+    return new Blob(goog.array.toArray(arguments));
   }
-  return bb.getBlob();
 };
 
 
diff --git a/third_party/closure/goog/functions/functions.js b/third_party/closure/goog/functions/functions.js
index 280e31c..85aee23 100644
--- a/third_party/closure/goog/functions/functions.js
+++ b/third_party/closure/goog/functions/functions.js
@@ -25,8 +25,9 @@
 
 /**
  * Creates a function that always returns the same value.
- * @param {*} retValue The value to return.
- * @return {!Function} The new function.
+ * @param {T} retValue The value to return.
+ * @return {function():T} The new function.
+ * @template T
  */
 goog.functions.constant = function(retValue) {
   return function() {
@@ -59,10 +60,10 @@
 /**
  * A simple function that returns the first argument of whatever is passed
  * into it.
- * @param {*=} opt_returnValue The single value that will be returned.
+ * @param {T=} opt_returnValue The single value that will be returned.
  * @param {...*} var_args Optional trailing arguments. These are ignored.
- * @return {?} The first argument passed in, or undefined if nothing was passed.
- *     We can't know the type -- just pass it along without type.
+ * @return {T} The first argument passed in, or undefined if nothing was passed.
+ * @template T
  */
 goog.functions.identity = function(opt_returnValue, var_args) {
   return opt_returnValue;
@@ -101,8 +102,9 @@
  * Given a function, create a new function that swallows its return value
  * and replaces it with a new one.
  * @param {Function} f A function.
- * @param {*} retValue A new return value.
- * @return {!Function} A new function.
+ * @param {T} retValue A new return value.
+ * @return {function(...[?]):T} A new function.
+ * @template T
  */
 goog.functions.withReturnValue = function(f, retValue) {
   return goog.functions.sequence(f, goog.functions.constant(retValue));
@@ -112,10 +114,12 @@
 /**
  * Creates the composition of the functions passed in.
  * For example, (goog.functions.compose(f, g))(a) is equivalent to f(g(a)).
+ * @param {function(...[?]):T} fn The final function.
  * @param {...Function} var_args A list of functions.
- * @return {!Function} The composition of all inputs.
+ * @return {function(...[?]):T} The composition of all inputs.
+ * @template T
  */
-goog.functions.compose = function(var_args) {
+goog.functions.compose = function(fn, var_args) {
   var functions = arguments;
   var length = functions.length;
   return function() {
@@ -158,7 +162,8 @@
  * short-circuited as soon as a function returns false.
  * For example, (goog.functions.and(f, g))(x) is equivalent to f(x) && g(x).
  * @param {...Function} var_args A list of functions.
- * @return {!Function} A function that ANDs its component functions.
+ * @return {function(...[?]):boolean} A function that ANDs its component
+ *      functions.
  */
 goog.functions.and = function(var_args) {
   var functions = arguments;
@@ -180,7 +185,8 @@
  * short-circuited as soon as a function returns true.
  * For example, (goog.functions.or(f, g))(x) is equivalent to f(x) || g(x).
  * @param {...Function} var_args A list of functions.
- * @return {!Function} A function that ORs its component functions.
+ * @return {function(...[?]):boolean} A function that ORs its component
+ *    functions.
  */
 goog.functions.or = function(var_args) {
   var functions = arguments;
@@ -200,7 +206,8 @@
  * Creates a function that returns the Boolean opposite of a provided function.
  * For example, (goog.functions.not(f))(x) is equivalent to !f(x).
  * @param {!Function} f The original function.
- * @return {!Function} A function that delegates to f and returns opposite.
+ * @return {function(...[?]):boolean} A function that delegates to f and returns
+ * opposite.
  */
 goog.functions.not = function(f) {
   return function() {
diff --git a/third_party/closure/goog/history/history.js b/third_party/closure/goog/history/history.js
index 6b4d650..1540f71 100644
--- a/third_party/closure/goog/history/history.js
+++ b/third_party/closure/goog/history/history.js
@@ -286,6 +286,7 @@
    * @private
    */
   this.timer_ = new goog.Timer(goog.History.PollingType.NORMAL);
+  this.registerDisposable(this.timer_);
 
   /**
    * True if the state tokens are displayed in the address bar, false for hidden
diff --git a/third_party/closure/goog/iter/iter.js b/third_party/closure/goog/iter/iter.js
index f270463..f9a0dcc 100644
--- a/third_party/closure/goog/iter/iter.js
+++ b/third_party/closure/goog/iter/iter.js
@@ -138,19 +138,23 @@
  * @param {goog.iter.Iterable} iterable  The iterator to iterate
  *     over.  If the iterable is an object {@code toIterator} will be called on
  *     it.
- * @param {Function} f  The function to call for every element.  This function
+* @param {function(this:T,?,?,?):?} f  The function to call for every
+ *     element.  This function
  *     takes 3 arguments (the element, undefined, and the iterator) and the
  *     return value is irrelevant.  The reason for passing undefined as the
  *     second argument is so that the same function can be used in
  *     {@see goog.array#forEach} as well as others.
- * @param {Object=} opt_obj  The object to be used as the value of 'this' within
+ * @param {T=} opt_obj  The object to be used as the value of 'this' within
  *     {@code f}.
+ * @template T
  */
 goog.iter.forEach = function(iterable, f, opt_obj) {
   if (goog.isArrayLike(iterable)) {
     /** @preserveTry */
     try {
-      goog.array.forEach((/** @type {goog.array.ArrayLike} */ iterable), f,
+      // NOTES: this passes the index number to the second parameter
+      // of the callback contrary to the documentation above.
+      goog.array.forEach(/** @type {goog.array.ArrayLike} */(iterable), f,
                          opt_obj);
     } catch (ex) {
       if (ex !== goog.iter.StopIteration) {
@@ -178,15 +182,17 @@
  * returns true adds the element to a new iterator.
  *
  * @param {goog.iter.Iterable} iterable The iterator to iterate over.
- * @param {Function} f The function to call for every element.  This function
+ * @param {function(this:T,?,undefined,?):boolean} f The function to call for
+ *     every element. This function
  *     takes 3 arguments (the element, undefined, and the iterator) and should
  *     return a boolean.  If the return value is true the element will be
  *     included  in the returned iteror.  If it is false the element is not
  *     included.
- * @param {Object=} opt_obj The object to be used as the value of 'this' within
+ * @param {T=} opt_obj The object to be used as the value of 'this' within
  *     {@code f}.
  * @return {!goog.iter.Iterator} A new iterator in which only elements that
  *     passed the test are present.
+ * @template T
  */
 goog.iter.filter = function(iterable, f, opt_obj) {
   var iterator = goog.iter.toIterator(iterable);
@@ -262,13 +268,15 @@
  * with that value.
  *
  * @param {goog.iter.Iterable} iterable The iterator to iterate over.
- * @param {Function} f The function to call for every element.  This function
+ * @param {function(this:T,?,undefined,?):?} f The function to call for every
+ *     element.  This function
  *     takes 3 arguments (the element, undefined, and the iterator) and should
  *     return a new value.
- * @param {Object=} opt_obj The object to be used as the value of 'this' within
+ * @param {T=} opt_obj The object to be used as the value of 'this' within
  *     {@code f}.
  * @return {!goog.iter.Iterator} A new iterator that returns the results of
  *     applying the function to each element in the original iterator.
+ * @template T
  */
 goog.iter.map = function(iterable, f, opt_obj) {
   var iterator = goog.iter.toIterator(iterable);
@@ -288,15 +296,16 @@
  * result.
  *
  * @param {goog.iter.Iterable} iterable The iterator to iterate over.
- * @param {Function} f The function to call for every element. This function
- *     takes 2 arguments (the function's previous result or the initial value,
- *     and the value of the current element).
+ * @param {function(this:T,V,?):V} f The function to call for every
+ *     element. This function takes 2 arguments (the function's previous result
+ *     or the initial value, and the value of the current element).
  *     function(previousValue, currentElement) : newValue.
- * @param {*} val The initial value to pass into the function on the first call.
- * @param {Object=} opt_obj  The object to be used as the value of 'this'
+ * @param {V} val The initial value to pass into the function on the first call.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
  *     within f.
- * @return {*} Result of evaluating f repeatedly across the values of
+ * @return {V} Result of evaluating f repeatedly across the values of
  *     the iterator.
+ * @template T,V
  */
 goog.iter.reduce = function(iterable, f, val, opt_obj) {
   var rval = val;
@@ -313,12 +322,14 @@
  * return false this will return false.
  *
  * @param {goog.iter.Iterable} iterable  The iterator object.
- * @param {Function} f  The function to call for every value. This function
+ * @param {function(this:T,?,undefined,?):boolean} f  The function to call for
+ *     every value. This function
  *     takes 3 arguments (the value, undefined, and the iterator) and should
  *     return a boolean.
- * @param {Object=} opt_obj The object to be used as the value of 'this' within
+ * @param {T=} opt_obj The object to be used as the value of 'this' within
  *     {@code f}.
  * @return {boolean} true if any value passes the test.
+ * @template T
  */
 goog.iter.some = function(iterable, f, opt_obj) {
   iterable = goog.iter.toIterator(iterable);
@@ -344,12 +355,14 @@
  * return true this will return true.
  *
  * @param {goog.iter.Iterable} iterable  The iterator object.
- * @param {Function} f  The function to call for every value. This function
+ * @param {function(this:T,?,undefined,?):boolean} f  The function to call for
+ *     every value. This function
  *     takes 3 arguments (the value, undefined, and the iterator) and should
  *     return a boolean.
- * @param {Object=} opt_obj The object to be used as the value of 'this' within
+ * @param {T=} opt_obj The object to be used as the value of 'this' within
  *     {@code f}.
  * @return {boolean} true if every value passes the test.
+ * @template T
  */
 goog.iter.every = function(iterable, f, opt_obj) {
   iterable = goog.iter.toIterator(iterable);
@@ -413,13 +426,15 @@
  * Builds a new iterator that iterates over the original, but skips elements as
  * long as a supplied function returns true.
  * @param {goog.iter.Iterable} iterable  The iterator object.
- * @param {Function} f  The function to call for every value. This function
+ * @param {function(this:T,?,undefined,?):boolean} f  The function to call for
+ *     every value. This function
  *     takes 3 arguments (the value, undefined, and the iterator) and should
  *     return a boolean.
- * @param {Object=} opt_obj The object to be used as the value of 'this' within
+ * @param {T=} opt_obj The object to be used as the value of 'this' within
  *     {@code f}.
  * @return {!goog.iter.Iterator} A new iterator that drops elements from the
  *     original iterator as long as {@code f} is true.
+ * @template T
  */
 goog.iter.dropWhile = function(iterable, f, opt_obj) {
   var iterator = goog.iter.toIterator(iterable);
@@ -444,12 +459,14 @@
  * Builds a new iterator that iterates over the original, but only as long as a
  * supplied function returns true.
  * @param {goog.iter.Iterable} iterable  The iterator object.
- * @param {Function} f  The function to call for every value. This function
+ * @param {function(this:T,?,undefined,?):boolean} f  The function to call for
+ *     every value. This function
  *     takes 3 arguments (the value, undefined, and the iterator) and should
  *     return a boolean.
- * @param {Object=} opt_obj This is used as the 'this' object in f when called.
+ * @param {T=} opt_obj This is used as the 'this' object in f when called.
  * @return {!goog.iter.Iterator} A new iterator that keeps elements in the
  *     original iterator as long as the function is true.
+ * @template T
  */
 goog.iter.takeWhile = function(iterable, f, opt_obj) {
   var iterator = goog.iter.toIterator(iterable);
@@ -481,7 +498,7 @@
 goog.iter.toArray = function(iterable) {
   // Fast path for array-like.
   if (goog.isArrayLike(iterable)) {
-    return goog.array.toArray((/** @type {!goog.array.ArrayLike} */ iterable));
+    return goog.array.toArray(/** @type {!goog.array.ArrayLike} */(iterable));
   }
   iterable = goog.iter.toIterator(iterable);
   var array = [];
@@ -656,7 +673,6 @@
 
     // Pull elements off the original iterator if not using cache
     if (!useCache) {
-
       try {
         // Return the element from the iterable
         returnElement = baseIterator.next();
diff --git a/third_party/closure/goog/json/json.js b/third_party/closure/goog/json/json.js
index 3fa6400..2a0b74c 100644
--- a/third_party/closure/goog/json/json.js
+++ b/third_party/closure/goog/json/json.js
@@ -61,7 +61,7 @@
   // Don't make these static since they have the global flag.
   var backslashesRe = /\\["\\\/bfnrtu]/g;
   var simpleValuesRe =
-      /"[^"\\\n\r\u2028\u2029\x00-\x08\x10-\x1f\x80-\x9f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
+      /"[^"\\\n\r\u2028\u2029\x00-\x08\x0a-\x1f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
   var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g;
   var remainderRe = /^[\],:{}\s\u2028\u2029]*$/;
 
diff --git a/third_party/closure/goog/locale/locale.js b/third_party/closure/goog/locale/locale.js
index c139b86..9506989 100644
--- a/third_party/closure/goog/locale/locale.js
+++ b/third_party/closure/goog/locale/locale.js
@@ -150,8 +150,8 @@
 goog.locale.getNativeCountryName = function(countryCode) {
   var key = goog.locale.getLanguageSubTag(countryCode) + '_' +
             goog.locale.getRegionSubTag(countryCode);
-  return key in goog.locale.nativeNameConstants.COUNTRY ?
-      goog.locale.nativeNameConstants.COUNTRY[key] : countryCode;
+  return key in goog.locale.nativeNameConstants['COUNTRY'] ?
+      goog.locale.nativeNameConstants['COUNTRY'][key] : countryCode;
 };
 
 
@@ -193,9 +193,11 @@
  * @return {string} Language name for the provided language code.
  */
 goog.locale.getNativeLanguageName = function(languageCode) {
+  if (languageCode in goog.locale.nativeNameConstants['LANGUAGE'])
+    return goog.locale.nativeNameConstants['LANGUAGE'][languageCode];
   var code = goog.locale.getLanguageSubTag(languageCode);
-  return code in goog.locale.nativeNameConstants.LANGUAGE ?
-      goog.locale.nativeNameConstants.LANGUAGE[code] : languageCode;
+  return code in goog.locale.nativeNameConstants['LANGUAGE'] ?
+      goog.locale.nativeNameConstants['LANGUAGE'][code] : languageCode;
 };
 
 
@@ -218,6 +220,8 @@
     opt_localeSymbols = goog.locale.getResource('LocaleNameConstants',
         goog.locale.getLocale());
   }
+  if (languageCode in opt_localeSymbols['LANGUAGE'])
+    return opt_localeSymbols['LANGUAGE'][languageCode];
   var code = goog.locale.getLanguageSubTag(languageCode);
   return code in opt_localeSymbols['LANGUAGE'] ?
       opt_localeSymbols['LANGUAGE'][code] : languageCode;
diff --git a/third_party/closure/goog/messaging/respondingchannel.js b/third_party/closure/goog/messaging/respondingchannel.js
index 50775ef..3ac1912 100644
--- a/third_party/closure/goog/messaging/respondingchannel.js
+++ b/third_party/closure/goog/messaging/respondingchannel.js
@@ -141,6 +141,8 @@
 goog.messaging.RespondingChannel.prototype.disposeInternal = function() {
   goog.dispose(this.messageChannel_);
   delete this.messageChannel_;
+  // Note: this.publicChannel_ and this.privateChannel_ get disposed by
+  //     this.messageChannel_
   delete this.publicChannel_;
   delete this.privateChannel_;
 };
@@ -224,8 +226,11 @@
   var resultMessage = {};
   resultMessage['data'] = callback(message['data']);
   resultMessage['signature'] = message['signature'];
-
-  this.privateChannel_.send(
-      goog.messaging.RespondingChannel.CALLBACK_SERVICE_,
-      resultMessage);
+  // The callback invoked above may have disposed the channel so check if it
+  // exists.
+  if (this.privateChannel_) {
+    this.privateChannel_.send(
+        goog.messaging.RespondingChannel.CALLBACK_SERVICE_,
+        resultMessage);
+  }
 };
diff --git a/third_party/closure/goog/module/modulemanager.js b/third_party/closure/goog/module/modulemanager.js
index 6b445e1..43503d6 100644
--- a/third_party/closure/goog/module/modulemanager.js
+++ b/third_party/closure/goog/module/modulemanager.js
@@ -27,6 +27,7 @@
 goog.require('goog.async.Deferred');
 goog.require('goog.debug.Logger');
 goog.require('goog.debug.Trace');
+goog.require('goog.dispose');
 goog.require('goog.module.ModuleInfo');
 goog.require('goog.module.ModuleLoadCallback');
 goog.require('goog.object');
@@ -1247,7 +1248,8 @@
   goog.module.ModuleManager.superClass_.disposeInternal.call(this);
 
   // Dispose of each ModuleInfo object.
-  goog.array.forEach(goog.object.getValues(this.moduleInfoMap_), goog.dispose);
+  goog.disposeAll(
+      goog.object.getValues(this.moduleInfoMap_), this.baseModuleInfo_);
   this.moduleInfoMap_ = null;
   this.loadingModuleIds_ = null;
   this.requestedLoadingModuleIds_ = null;
diff --git a/third_party/closure/goog/net/browserchannel.js b/third_party/closure/goog/net/browserchannel.js
index 6d3bbba..3efe031 100644
--- a/third_party/closure/goog/net/browserchannel.js
+++ b/third_party/closure/goog/net/browserchannel.js
@@ -401,6 +401,33 @@
 
 
 /**
+ * A throttle time in ms for readystatechange events for the backchannel.
+ * Useful for throttling when ready state is INTERACTIVE (partial data).
+ *
+ * This throttle is useful if the server sends large data chunks down the
+ * backchannel.  It prevents examining XHR partial data on every
+ * readystate change event.  This is useful because large chunks can
+ * trigger hundreds of readystatechange events, each of which takes ~5ms
+ * or so to handle, in turn making the UI unresponsive for a significant period.
+ *
+ * If set to zero no throttle is used.
+ * @type {number}
+ * @private
+ */
+goog.net.BrowserChannel.prototype.readyStateChangeThrottleMs_ = 0;
+
+
+/**
+ * Whether cross origin requests are supported for the browser channel.
+ *
+ * See {@link goog.net.XhrIo#setWithCredentials}.
+ * @type {boolean}
+ * @private
+ */
+goog.net.BrowserChannel.prototype.supportsCrossDomainXhrs_ = false;
+
+
+/**
  * The latest protocol version that this class supports. We request this version
  * from the server when opening the connection. Should match
  * com.google.net.browserchannel.BrowserChannel.LATEST_CHANNEL_VERSION.
@@ -1028,6 +1055,37 @@
 
 
 /**
+ * Sets the throttle for handling onreadystatechange events for the request.
+ *
+ * @param {number} throttle The throttle in ms.  A value of zero indicates
+ *     no throttle.
+ */
+goog.net.BrowserChannel.prototype.setReadyStateChangeThrottle = function(
+    throttle) {
+  this.readyStateChangeThrottleMs_ = throttle;
+};
+
+
+/**
+ * Sets whether cross origin requests are supported for the browser channel.
+ *
+ * Setting this allows the creation of requests to secondary domains and
+ * sends XHRs with the CORS withCredentials bit set to true.
+ *
+ * In order for cross-origin requests to work, the server will also need to set
+ * CORS response headers as per:
+ * https://developer.mozilla.org/en-US/docs/HTTP_access_control
+ *
+ * See {@link goog.net.XhrIo#setWithCredentials}.
+ * @param {boolean} supportCrossDomain Whether cross domain XHRs are supported.
+ */
+goog.net.BrowserChannel.prototype.setSupportsCrossDomainXhrs = function(
+    supportCrossDomain) {
+  this.supportsCrossDomainXhrs_ = supportCrossDomain;
+};
+
+
+/**
  * Returns the handler used for channel callback events.
  *
  * @return {goog.net.BrowserChannel.Handler} The handler.
@@ -1620,6 +1678,8 @@
       'rpc',
       this.backChannelAttemptId_);
   this.backChannelRequest_.setExtraHeaders(this.extraHeaders_);
+  this.backChannelRequest_.setReadyStateChangeThrottle(
+      this.readyStateChangeThrottleMs_);
   var uri = this.backChannelUri_.clone();
   uri.setParameterValue('RID', 'rpc');
   uri.setParameterValue('SID', this.sid_);
@@ -2282,17 +2342,19 @@
 /**
  * Called when BC needs to create an XhrIo object.  Override in a subclass if
  * you need to customize the behavior, for example to enable the creation of
- * XHR's capable of calling a secondary domain.
+ * XHR's capable of calling a secondary domain. Will also allow calling
+ * a secondary domain if withCredentials (CORS) is enabled.
  * @param {?string} hostPrefix The host prefix, if we need an XhrIo object
  *     capable of calling a secondary domain.
  * @return {!goog.net.XhrIo} A new XhrIo object.
  */
 goog.net.BrowserChannel.prototype.createXhrIo = function(hostPrefix) {
-  if (hostPrefix) {
-    throw new Error('Can\'t create secondary domain capable XhrIo object.');
-  } else {
-    return new goog.net.XhrIo();
+  if (hostPrefix && !this.supportsCrossDomainXhrs_) {
+    throw Error('Can\'t create secondary domain capable XhrIo object.');
   }
+  var xhr = new goog.net.XhrIo();
+  xhr.setWithCredentials(this.supportsCrossDomainXhrs_);
+  return xhr;
 };
 
 
@@ -2397,22 +2459,27 @@
  * a host prefix. This allows us to work around browser per-domain
  * connection limits.
  *
- * Currently, we only use secondary domains when using Trident's ActiveXObject,
- * because it supports cross-domain requests out of the box. Even if we wanted
- * to use secondary domains on Gecko/Webkit, they wouldn't work due to
- * security restrictions on cross-origin XHRs. Note that in IE10 we no longer
- * use ActiveX since it's not supported in Metro mode and IE10 supports XHR
- * streaming.
+ * Currently, we  use secondary domains when using Trident's ActiveXObject,
+ * because it supports cross-domain requests out of the box.  Note that in IE10
+ * we no longer use ActiveX since it's not supported in Metro mode and IE10
+ * supports XHR streaming.
  *
- * If you need to use secondary domains on other browsers, you'll need
- * to override this method in a subclass, and make sure that those browsers
- * use some messaging mechanism that works cross-domain.
+ * If you need to use secondary domains on other browsers and IE10,
+ * you have two choices:
+ *     1) If you only care about browsers that support CORS
+ *        (https://developer.mozilla.org/en-US/docs/HTTP_access_control), you
+ *        can use {@link #setSupportsCrossDomainXhrs} and set the appropriate
+ *        CORS response headers on the server.
+ *     2) Or, override this method in a subclass, and make sure that those
+ *        browsers use some messaging mechanism that works cross-domain (e.g
+ *        iframes and window.postMessage).
  *
  * @return {boolean} Whether to use secondary domains.
  * @see http://code.google.com/p/closure-library/issues/detail?id=339
  */
 goog.net.BrowserChannel.prototype.shouldUseSecondaryDomains = function() {
-  return !goog.net.ChannelRequest.supportsXhrStreaming();
+  return this.supportsCrossDomainXhrs_ ||
+      !goog.net.ChannelRequest.supportsXhrStreaming();
 };
 
 
diff --git a/third_party/closure/goog/net/channelrequest.js b/third_party/closure/goog/net/channelrequest.js
index b09ba97..78701b8 100644
--- a/third_party/closure/goog/net/channelrequest.js
+++ b/third_party/closure/goog/net/channelrequest.js
@@ -28,6 +28,7 @@
 goog.provide('goog.net.ChannelRequest.Error');
 
 goog.require('goog.Timer');
+goog.require('goog.async.Throttle');
 goog.require('goog.events');
 goog.require('goog.events.EventHandler');
 goog.require('goog.net.EventType');
@@ -255,6 +256,28 @@
 
 
 /**
+ * A throttle time in ms for readystatechange events for the backchannel.
+ * Useful for throttling when ready state is INTERACTIVE (partial data).
+ * If set to zero no throttle is used.
+ *
+ * @see goog.net.BrowserChannel.prototype.readyStateChangeThrottleMs_
+ *
+ * @type {number}
+ * @private
+ */
+goog.net.ChannelRequest.prototype.readyStateChangeThrottleMs_ = 0;
+
+
+/**
+ * The throttle for readystatechange events for the current request, or null
+ * if there is none.
+ * @type {goog.async.Throttle}
+ * @private
+ */
+goog.net.ChannelRequest.prototype.readyStateChangeThrottle_ = null;
+
+
+/**
  * Default timeout in MS for a request. The server must return data within this
  * time limit for the request to not timeout.
  * @type {number}
@@ -422,6 +445,18 @@
 
 
 /**
+ * Sets the throttle for handling onreadystatechange events for the request.
+ *
+ * @param {number} throttle The throttle in ms.  A value of zero indicates
+ *     no throttle.
+ */
+goog.net.ChannelRequest.prototype.setReadyStateChangeThrottle = function(
+    throttle) {
+  this.readyStateChangeThrottleMs_ = throttle;
+};
+
+
+/**
  * Uses XMLHTTP to send an HTTP POST to the server.
  *
  * @param {goog.Uri} uri  The uri of the request.
@@ -486,9 +521,16 @@
   var useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();
   this.xmlHttp_ = this.channel_.createXhrIo(useSecondaryDomains ?
       hostPrefix : null);
+
+  if (this.readyStateChangeThrottleMs_ > 0) {
+    this.readyStateChangeThrottle_ = new goog.async.Throttle(
+        goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),
+        this.readyStateChangeThrottleMs_);
+  }
+
   this.eventHandler_.listen(this.xmlHttp_,
       goog.net.EventType.READY_STATE_CHANGE,
-      this.xmlHttpHandler_, false, this);
+      this.readyStateChangeHandler_);
 
   var headers = this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};
   if (this.postData_) {
@@ -517,12 +559,31 @@
 
 
 /**
- * XmlHttp handler
- * @param {goog.events.Event} e Event object, target is a XhrIo object.
+ * Handles a readystatechange event.
+ * @param {goog.events.Event} evt The event.
  * @private
  */
-goog.net.ChannelRequest.prototype.xmlHttpHandler_ = function(e) {
-  var xmlhttp = e.target;
+goog.net.ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {
+  var xhr = /** @type {goog.net.XhrIo} */ (evt.target);
+  var throttle = this.readyStateChangeThrottle_;
+  if (throttle &&
+      xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
+    // Only throttle in the partial data case.
+    this.channelDebug_.debug('Throttling readystatechange.');
+    throttle.fire();
+  } else {
+    // If we haven't throttled, just handle response directly.
+    this.xmlHttpHandler_(xhr);
+  }
+};
+
+
+/**
+ * XmlHttp handler
+ * @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.
+ * @private
+ */
+goog.net.ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {
   goog.net.BrowserChannel.onStartExecution();
   /** @preserveTry */
   try {
@@ -1101,6 +1162,9 @@
 goog.net.ChannelRequest.prototype.cleanup_ = function() {
   this.cancelWatchDogTimer_();
 
+  goog.dispose(this.readyStateChangeThrottle_);
+  this.readyStateChangeThrottle_ = null;
+
   // Stop the polling timer, if necessary.
   this.pollingTimer_.stop();
 
diff --git a/third_party/closure/goog/net/cookies.js b/third_party/closure/goog/net/cookies.js
index 10cc5b8..4ab6fb6 100644
--- a/third_party/closure/goog/net/cookies.js
+++ b/third_party/closure/goog/net/cookies.js
@@ -22,8 +22,6 @@
 goog.provide('goog.net.Cookies');
 goog.provide('goog.net.cookies');
 
-goog.require('goog.userAgent');
-
 
 
 /**
@@ -190,7 +188,8 @@
   var nameEq = name + '=';
   var parts = this.getParts_();
   for (var i = 0, part; part = parts[i]; i++) {
-    if (part.indexOf(nameEq) == 0) {
+    // startsWith
+    if (part.lastIndexOf(nameEq, 0) == 0) {
       return part.substr(nameEq.length);
     }
     if (part == name) {
diff --git a/third_party/closure/goog/net/iframeio.js b/third_party/closure/goog/net/iframeio.js
index deb8b39..ae12bcb 100644
--- a/third_party/closure/goog/net/iframeio.js
+++ b/third_party/closure/goog/net/iframeio.js
@@ -141,6 +141,7 @@
 goog.require('goog.debug.Logger');
 goog.require('goog.dom');
 goog.require('goog.events');
+goog.require('goog.events.Event');
 goog.require('goog.events.EventTarget');
 goog.require('goog.events.EventType');
 goog.require('goog.json');
@@ -344,8 +345,9 @@
  * @private
  */
 goog.net.IframeIo.addFormInputs_ = function(form, data) {
+  var helper = goog.dom.getDomHelper(form);
   goog.structs.forEach(data, function(value, key) {
-    var inp = goog.dom.createDom('input',
+    var inp = helper.createDom('input',
         {'type': 'hidden', 'name': key, 'value': value});
     form.appendChild(inp);
   });
@@ -1114,8 +1116,8 @@
     iframeAttributes.src = 'javascript:""';
   }
 
-  this.iframe_ = /** @type {HTMLIFrameElement} */(goog.dom.createDom(
-      'iframe', iframeAttributes));
+  this.iframe_ = /** @type {HTMLIFrameElement} */(
+      goog.dom.getDomHelper(this.form_).createDom('iframe', iframeAttributes));
 
   var s = this.iframe_.style;
   s.visibility = 'hidden';
@@ -1137,7 +1139,8 @@
  * @private
  */
 goog.net.IframeIo.prototype.appendIframe_ = function() {
-  goog.dom.getDocument().body.appendChild(this.iframe_);
+  goog.dom.getDomHelper(this.form_).getDocument().body.appendChild(
+      this.iframe_);
 };
 
 
diff --git a/third_party/closure/goog/net/xhrio.js b/third_party/closure/goog/net/xhrio.js
index 3be2347..534a9ef 100644
--- a/third_party/closure/goog/net/xhrio.js
+++ b/third_party/closure/goog/net/xhrio.js
@@ -156,9 +156,12 @@
  *     request.
  * @param {number=} opt_timeoutInterval Number of milliseconds after which an
  *     incomplete request will be aborted; 0 means no timeout is set.
+ * @param {boolean=} opt_withCredentials Whether to send credentials with the
+ *     request. Default to false. See {@link goog.net.XhrIo#setWithCredentials}.
  */
 goog.net.XhrIo.send = function(url, opt_callback, opt_method, opt_content,
-                               opt_headers, opt_timeoutInterval) {
+                               opt_headers, opt_timeoutInterval,
+                               opt_withCredentials) {
   var x = new goog.net.XhrIo();
   goog.net.XhrIo.sendInstances_.push(x);
   if (opt_callback) {
@@ -170,6 +173,9 @@
   if (opt_timeoutInterval) {
     x.setTimeoutInterval(opt_timeoutInterval);
   }
+  if (opt_withCredentials) {
+    x.setWithCredentials(opt_withCredentials);
+  }
   x.send(url, opt_method, opt_content, opt_headers);
 };
 
@@ -444,7 +450,8 @@
 goog.net.XhrIo.prototype.send = function(url, opt_method, opt_content,
                                          opt_headers) {
   if (this.xhr_) {
-    throw Error('[goog.net.XhrIo] Object is active with another request');
+    throw Error('[goog.net.XhrIo] Object is active with another request=' +
+        this.lastUri_ + '; newUri=' + url);
   }
 
   var method = opt_method ? opt_method.toUpperCase() : 'GET';
diff --git a/third_party/closure/goog/net/xhrmanager.js b/third_party/closure/goog/net/xhrmanager.js
index ee68f65..d7b786b 100644
--- a/third_party/closure/goog/net/xhrmanager.js
+++ b/third_party/closure/goog/net/xhrmanager.js
@@ -178,6 +178,8 @@
  *     complete. The only param is the event object from the COMPLETE event.
  * @param {number=} opt_maxRetries The maximum number of times the request
  *     should be retried.
+ * @param {goog.net.XhrIo.ResponseType=} opt_responseType The response type of
+ *     this request; defaults to goog.net.XhrIo.ResponseType.DEFAULT.
  * @return {goog.net.XhrManager.Request} The queued request object.
  */
 goog.net.XhrManager.prototype.send = function(
@@ -188,7 +190,8 @@
     opt_headers,
     opt_priority,
     opt_callback,
-    opt_maxRetries) {
+    opt_maxRetries,
+    opt_responseType) {
   var requests = this.requests_;
   // Check if there is already a request with the given id.
   if (requests.get(id)) {
@@ -203,7 +206,8 @@
       opt_content,
       opt_headers,
       opt_callback,
-      goog.isDef(opt_maxRetries) ? opt_maxRetries : this.maxRetries_);
+      goog.isDef(opt_maxRetries) ? opt_maxRetries : this.maxRetries_,
+      opt_responseType);
   this.requests_.set(id, request);
 
   // Setup the callback for the pool.
@@ -263,6 +267,7 @@
 
     // Set properties for the XhrIo.
     xhrIo.setTimeoutInterval(this.timeoutInterval_);
+    xhrIo.setResponseType(request.getResponseType());
 
     // Add a reference to the XhrIo object to the request.
     request.xhrIo = request.xhrLite = xhrIo;
@@ -546,12 +551,14 @@
  *     complete. NOTE: Only 1 callback supported across all events.
  * @param {number=} opt_maxRetries The maximum number of times the request
  *     should be retried (Default: 1).
+ * @param {goog.net.XhrIo.ResponseType=} opt_responseType The response type of
+ *     this request; defaults to goog.net.XhrIo.ResponseType.DEFAULT.
  *
  * @constructor
  * @extends {goog.Disposable}
  */
 goog.net.XhrManager.Request = function(url, xhrEventCallback, opt_method,
-    opt_content, opt_headers, opt_callback, opt_maxRetries) {
+    opt_content, opt_headers, opt_callback, opt_maxRetries, opt_responseType) {
   goog.Disposable.call(this);
 
   /**
@@ -625,6 +632,13 @@
   this.completeCallback_ = opt_callback;
 
   /**
+   * A response type to set on this.xhrIo when it's populated.
+   * @type {!goog.net.XhrIo.ResponseType}
+   * @private
+   */
+  this.responseType_ = opt_responseType || goog.net.XhrIo.ResponseType.DEFAULT;
+
+  /**
    * The XhrIo instance handling this request. Set in handleAvailableXhr.
    * @type {goog.net.XhrIo}
    */
@@ -761,6 +775,17 @@
 };
 
 
+/**
+ * Gets the response type that will be set on this request's XhrIo when it's
+ * available.
+ * @return {!goog.net.XhrIo.ResponseType} The response type to be set
+ *     when an XhrIo becomes available to this request.
+ */
+goog.net.XhrManager.Request.prototype.getResponseType = function() {
+  return this.responseType_;
+};
+
+
 /** @override */
 goog.net.XhrManager.Request.prototype.disposeInternal = function() {
   goog.net.XhrManager.Request.superClass_.disposeInternal.call(this);
diff --git a/third_party/closure/goog/net/xpc/crosspagechannel.js b/third_party/closure/goog/net/xpc/crosspagechannel.js
index 6c5645e..2efb366 100644
--- a/third_party/closure/goog/net/xpc/crosspagechannel.js
+++ b/third_party/closure/goog/net/xpc/crosspagechannel.js
@@ -109,7 +109,7 @@
       goog.uri.utils.getHost(cfg[goog.net.xpc.CfgFields.PEER_URI] || '') +
           '/robots.txt';
 
-  goog.net.xpc.channels_[this.name] = this;
+  goog.net.xpc.channels[this.name] = this;
 
   goog.events.listen(window, 'unload',
       goog.net.xpc.CrossPageChannel.disposeAll_);
@@ -199,6 +199,28 @@
 
 
 /**
+ * Returns the configuration object for this channel.
+ * Package private. Do not call from outside goog.net.xpc.
+ *
+ * @return {Object} The configuration object for this channel.
+ */
+goog.net.xpc.CrossPageChannel.prototype.getConfig = function() {
+  return this.cfg_;
+};
+
+
+/**
+ * Returns a reference to the iframe-element.
+ * Package private. Do not call from outside goog.net.xpc.
+ *
+ * @return {Object} A reference to the iframe-element.
+ */
+goog.net.xpc.CrossPageChannel.prototype.getIframeElement = function() {
+  return this.iframeElement_;
+};
+
+
+/**
  * Sets the window object the foreign document resides in.
  *
  * @param {Object} peerWindowObject The window object of the peer.
@@ -211,7 +233,7 @@
 
 /**
  * Returns the window object the foreign document resides in.
- * Package private.
+ * Package private. Do not call from outside goog.net.xpc.
  *
  * @return {Object} The window object of the peer.
  */
@@ -222,7 +244,7 @@
 
 /**
  * Determines whether the peer window is available (e.g. not closed).
- * Package private.
+ * Package private. Do not call from outside goog.net.xpc.
  *
  * @return {boolean} Whether the peer window is available.
  */
@@ -599,9 +621,9 @@
 
 /**
  * Called by the transport in case of an unrecoverable failure.
- * @private
+ * Package private. Do not call from outside goog.net.xpc.
  */
-goog.net.xpc.CrossPageChannel.prototype.notifyTransportError_ = function() {
+goog.net.xpc.CrossPageChannel.prototype.notifyTransportError = function() {
   goog.net.xpc.logger.info('Transport Error');
   this.close();
 };
@@ -631,7 +653,11 @@
 
 
 /**
- * Delivers messages to the appropriate service-handler.
+ * Delivers messages to the appropriate service-handler. Named xpcDeliver to
+ * avoid name conflict with {@code deliver} function in superclass
+ * goog.messaging.AbstractChannel.
+ *
+ * Package private. Do not call from outside goog.net.xpc.
  *
  * @param {string} serviceName The name of the port.
  * @param {string} payload The payload.
@@ -640,34 +666,17 @@
  *     the PEER_HOSTNAME parameter was provided, they must match or the message
  *     will be rejected.
  */
-goog.net.xpc.CrossPageChannel.prototype.safeDeliver = function(
-    serviceName, payload, opt_origin) {
-  this.deliver_(serviceName, payload, opt_origin);
-};
-
-
-/**
- * Delivers messages to the appropriate service-handler.
- *
- * @param {string} serviceName The name of the port.
- * @param {string} payload The payload.
- * @param {string=} opt_origin An optional origin for the message, where the
- *     underlying transport makes that available.  If this is specified, and
- *     the PEER_HOSTNAME parameter was provided, they must match or the message
- *     will be rejected.
- * @private
- */
-goog.net.xpc.CrossPageChannel.prototype.deliver_ = function(
+goog.net.xpc.CrossPageChannel.prototype.xpcDeliver = function(
     serviceName, payload, opt_origin) {
 
-  // This covers the very rare (but producable) case where the inner frame
+  // This check covers the very rare (but producable) case where the inner frame
   // becomes ready and sends its setup message while the outer frame is
-  // deferring its connect method waiting for the inner frame to be ready.
-  // Without it that message can be passed to deliver_, which is unable to
-  // process it because the channel is not yet fully configured.
+  // deferring its connect method waiting for the inner frame to be ready. The
+  // resulting deferral ensures the message will not be processed until the
+  // channel is fully configured.
   if (this.peerWindowDeferred_) {
     this.deferredDeliveries_.push(
-        goog.bind(this.deliver_, this, serviceName, payload, opt_origin));
+        goog.bind(this.xpcDeliver, this, serviceName, payload, opt_origin));
     return;
   }
 
@@ -679,7 +688,7 @@
   }
 
   if (this.isDisposed()) {
-    goog.net.xpc.logger.warning('CrossPageChannel::deliver_(): Disposed.');
+    goog.net.xpc.logger.warning('CrossPageChannel::xpcDeliver(): Disposed.');
   } else if (!serviceName ||
       serviceName == goog.net.xpc.TRANSPORT_SERVICE_) {
     this.transport_.transportServiceHandler(payload);
@@ -688,7 +697,8 @@
     if (this.isConnected()) {
       this.deliver(this.unescapeServiceName_(serviceName), payload);
     } else {
-      goog.net.xpc.logger.info('CrossPageChannel::deliver_(): Not connected.');
+      goog.net.xpc.logger.info(
+          'CrossPageChannel::xpcDeliver(): Not connected.');
     }
   }
 };
@@ -772,7 +782,7 @@
 
   this.peerWindowObject_ = null;
   this.iframeElement_ = null;
-  delete goog.net.xpc.channels_[this.name];
+  delete goog.net.xpc.channels[this.name];
   goog.dispose(this.peerLoadHandler_);
   delete this.peerLoadHandler_;
   goog.base(this, 'disposeInternal');
@@ -784,7 +794,7 @@
  * @private
  */
 goog.net.xpc.CrossPageChannel.disposeAll_ = function() {
-  for (var name in goog.net.xpc.channels_) {
-    goog.dispose(goog.net.xpc.channels_[name]);
+  for (var name in goog.net.xpc.channels) {
+    goog.dispose(goog.net.xpc.channels[name]);
   }
 };
diff --git a/third_party/closure/goog/net/xpc/frameelementmethodtransport.js b/third_party/closure/goog/net/xpc/frameelementmethodtransport.js
index 650da64..0b601dd 100644
--- a/third_party/closure/goog/net/xpc/frameelementmethodtransport.js
+++ b/third_party/closure/goog/net/xpc/frameelementmethodtransport.js
@@ -81,7 +81,7 @@
  * @override
  */
 goog.net.xpc.FrameElementMethodTransport.prototype.transportType =
-   goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
+    goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
 
 
 /**
@@ -116,7 +116,7 @@
 goog.net.xpc.FrameElementMethodTransport.prototype.connect = function() {
   if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
     // get shortcut to iframe-element
-    this.iframeElm_ = this.channel_.iframeElement_;
+    this.iframeElm_ = this.channel_.getIframeElement();
 
     // add the gateway function to the iframe-element
     // (to be called by the peer)
@@ -205,7 +205,7 @@
 goog.net.xpc.FrameElementMethodTransport.prototype.incoming_ =
     function(serviceName, payload) {
   if (!this.recursive_ && this.queue_.length == 0) {
-    this.channel_.deliver_(serviceName, payload);
+    this.channel_.xpcDeliver(serviceName, payload);
   }
   else {
     this.queue_.push({serviceName: serviceName, payload: payload});
@@ -224,7 +224,7 @@
     function() {
   while (this.queue_.length) {
     var msg = this.queue_.shift();
-    this.channel_.deliver_(msg.serviceName, msg.payload);
+    this.channel_.xpcDeliver(msg.serviceName, msg.payload);
   }
 };
 
diff --git a/third_party/closure/goog/net/xpc/iframepollingtransport.js b/third_party/closure/goog/net/xpc/iframepollingtransport.js
index 5944b8e..9f52c44 100644
--- a/third_party/closure/goog/net/xpc/iframepollingtransport.js
+++ b/third_party/closure/goog/net/xpc/iframepollingtransport.js
@@ -60,14 +60,16 @@
    * @type {string}
    * @private
    */
-  this.sendUri_ = this.channel_.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI];
+  this.sendUri_ =
+      this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_POLL_URI];
 
   /**
    * The URI which is polled for incoming messages.
    * @type {string}
    * @private
    */
-  this.rcvUri_ = this.channel_.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
+  this.rcvUri_ =
+      this.channel_.getConfig()[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
 
   /**
    * The queue to hold messages which can't be sent immediately.
@@ -174,7 +176,7 @@
 /**
  * Safely retrieves the frames from the peer window. If an error is thrown
  * (e.g. the window is closing) an empty frame object is returned.
- * @return {!Object.<!Window>}
+ * @return {!Object.<!Window>} The frames from the peer window.
  * @private
  */
 goog.net.xpc.IframePollingTransport.prototype.getPeerFrames_ = function() {
@@ -193,7 +195,8 @@
 /**
  * Safely retrieves the peer frame with the specified name.
  * @param {string} frameName The name of the peer frame to retrieve.
- * @return {Window}
+ * @return {Window} The peer frame with the specified name.
+ * @private
  */
 goog.net.xpc.IframePollingTransport.prototype.getPeerFrame_ = function(
     frameName) {
@@ -474,7 +477,7 @@
 
       for (var i = 0, m; i < this.deliveryQueue_.length; i++) {
         m = this.deliveryQueue_[i];
-        this.channel_.deliver_(m.service, m.payload);
+        this.channel_.xpcDeliver(m.service, m.payload);
       }
       delete this.deliveryQueue_;
     }
@@ -619,7 +622,7 @@
         push({service: service, payload: payload});
     goog.net.xpc.logger.finest('queued delivery');
   } else {
-    this.channel_.deliver_(service, payload);
+    this.channel_.xpcDeliver(service, payload);
   }
 };
 
@@ -722,23 +725,25 @@
  * @private
  */
 goog.net.xpc.IframePollingTransport.receive_ = function() {
+  var receivers = goog.net.xpc.IframePollingTransport.receivers_;
+  var receiver;
   var rcvd = false;
+
   /** @preserveTry */
   try {
-    for (var i = 0, l = goog.net.xpc.IframePollingTransport.receivers_.length;
-         i < l; i++) {
-      rcvd = rcvd ||
-          goog.net.xpc.IframePollingTransport.receivers_[i].receive();
+    for (var i = 0; receiver = receivers[i]; i++) {
+      rcvd = rcvd || receiver.receive();
     }
   } catch (e) {
     goog.net.xpc.logger.info('receive_() failed: ' + e);
+
     // Notify the channel that the transport had an error.
-    goog.net.xpc.IframePollingTransport.receivers_[i].
-        transport_.channel_.notifyTransportError_();
-    // notifyTransportError_() closes the channel and dispoases the transport.
+    receiver.transport_.channel_.notifyTransportError();
+
+    // notifyTransportError() closes the channel and disposes the transport.
     // If there are no other channels present, this.receivers_ will now be empty
-    // and there is not need to keep polling.
-    if (!goog.net.xpc.IframePollingTransport.receivers_.length) {
+    // and there is no need to keep polling.
+    if (!receivers.length) {
       return;
     }
   }
@@ -856,7 +861,8 @@
  * goog.net.xpc.IframePollingTransport.Receiver
  *
  * @constructor
- * @param {goog.net.xpc.Transport} transport The transport to receive from.
+ * @param {goog.net.xpc.IframePollingTransport} transport The transport to
+ *     receive from.
  * @param {Object} windowObj The window-object to poll for location-changes.
  * @param {Function} callback The callback-function to be called when
  *     location has changed.
@@ -864,7 +870,11 @@
 goog.net.xpc.IframePollingTransport.Receiver = function(transport,
                                                         windowObj,
                                                         callback) {
-
+  /**
+   * The transport to receive from.
+   * @type {goog.net.xpc.IframePollingTransport}
+   * @private
+   */
   this.transport_ = transport;
   this.rcvFrame_ = windowObj;
 
diff --git a/third_party/closure/goog/net/xpc/iframerelaytransport.js b/third_party/closure/goog/net/xpc/iframerelaytransport.js
index 78892f2..1281c49 100644
--- a/third_party/closure/goog/net/xpc/iframerelaytransport.js
+++ b/third_party/closure/goog/net/xpc/iframerelaytransport.js
@@ -56,14 +56,15 @@
    * @private
    */
   this.peerRelayUri_ =
-      this.channel_.cfg_[goog.net.xpc.CfgFields.PEER_RELAY_URI];
+      this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_RELAY_URI];
 
   /**
    * The id of the iframe the peer page lives in.
    * @type {string}
    * @private
    */
-  this.peerIframeId_ = this.channel_.cfg_[goog.net.xpc.CfgFields.IFRAME_ID];
+  this.peerIframeId_ =
+      this.channel_.getConfig()[goog.net.xpc.CfgFields.IFRAME_ID];
 
   if (goog.userAgent.WEBKIT) {
     goog.net.xpc.IframeRelayTransport.startCleanupTimer_();
@@ -186,7 +187,7 @@
  * @override
  */
 goog.net.xpc.IframeRelayTransport.prototype.transportType =
-  goog.net.xpc.TransportTypes.IFRAME_RELAY;
+    goog.net.xpc.TransportTypes.IFRAME_RELAY;
 
 
 /**
@@ -250,13 +251,13 @@
     }
 
     // We've received all outstanding fragments; combine what we've received
-    // into payload and fall out to the call to deliver_.
+    // into payload and fall out to the call to xpcDeliver.
     payload = fragmentInfo.fragments.join('');
     delete goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr];
   }
 
-  goog.net.xpc.channels_[channelName].deliver_(service,
-                                               decodeURIComponent(payload));
+  goog.net.xpc.channels[channelName].
+      xpcDeliver(service, decodeURIComponent(payload));
 };
 
 
diff --git a/third_party/closure/goog/net/xpc/nativemessagingtransport.js b/third_party/closure/goog/net/xpc/nativemessagingtransport.js
index a22e5ed..e0674fa 100644
--- a/third_party/closure/goog/net/xpc/nativemessagingtransport.js
+++ b/third_party/closure/goog/net/xpc/nativemessagingtransport.js
@@ -311,9 +311,9 @@
   //  - channel was created in a different namespace
   //  - message was sent to the wrong window
   //  - channel has become stale (e.g. caching iframes and back clicks)
-  var channel = goog.net.xpc.channels_[channelName];
+  var channel = goog.net.xpc.channels[channelName];
   if (channel) {
-    channel.deliver_(service, payload, msgEvt.getBrowserEvent().origin);
+    channel.xpcDeliver(service, payload, msgEvt.getBrowserEvent().origin);
     return true;
   }
 
@@ -321,8 +321,8 @@
       goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload)[0];
 
   // Check if there are any stale channel names that can be updated.
-  for (var staleChannelName in goog.net.xpc.channels_) {
-    var staleChannel = goog.net.xpc.channels_[staleChannelName];
+  for (var staleChannelName in goog.net.xpc.channels) {
+    var staleChannel = goog.net.xpc.channels[staleChannelName];
     if (staleChannel.getRole() == goog.net.xpc.CrossPageChannelRole.INNER &&
         !staleChannel.isConnected() &&
         service == goog.net.xpc.TRANSPORT_SERVICE_ &&
@@ -338,10 +338,10 @@
       goog.net.xpc.logger.fine('changing channel name to ' + channelName);
       staleChannel.name = channelName;
       // Remove old stale pointer to channel.
-      delete goog.net.xpc.channels_[staleChannelName];
+      delete goog.net.xpc.channels[staleChannelName];
       // Create fresh pointer to channel.
-      goog.net.xpc.channels_[channelName] = staleChannel;
-      staleChannel.deliver_(service, payload);
+      goog.net.xpc.channels[channelName] = staleChannel;
+      staleChannel.xpcDeliver(service, payload);
       return true;
     }
   }
@@ -522,7 +522,7 @@
  */
 goog.net.xpc.NativeMessagingTransport.prototype.send = function(service,
                                                                 payload) {
-  var win = this.channel_.peerWindowObject_;
+  var win = this.channel_.getPeerWindowObject();
   if (!win) {
     goog.net.xpc.logger.fine('send(): window not ready');
     return;
diff --git a/third_party/closure/goog/net/xpc/nixtransport.js b/third_party/closure/goog/net/xpc/nixtransport.js
index 62080d1..7e00b7b 100644
--- a/third_party/closure/goog/net/xpc/nixtransport.js
+++ b/third_party/closure/goog/net/xpc/nixtransport.js
@@ -39,6 +39,7 @@
 goog.require('goog.reflect');
 
 
+
 /**
  * NIX method transport.
  *
@@ -153,7 +154,7 @@
     window.opener = /** @type {Window} */ ({});
     isSupported = goog.reflect.canAccessProperty(window, 'opener');
     window.opener = oldOpener;
-  } catch(e) { }
+  } catch (e) { }
   return isSupported;
 };
 
@@ -165,6 +166,7 @@
  * Note that this method can be called multiple times, as
  * it internally checks whether the work is necessary before
  * proceeding.
+ * @param {Window} listenWindow The window containing the affected page.
  * @private
  */
 goog.net.xpc.NixTransport.conductGlobalSetup_ = function(listenWindow) {
@@ -174,79 +176,79 @@
 
   // Inject the VBScript code needed.
   var vbscript =
-    // We create a class to act as a wrapper for
-    // a Javascript call, to prevent a break in of
-    // the context.
-    'Class ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n ' +
+      // We create a class to act as a wrapper for
+      // a Javascript call, to prevent a break in of
+      // the context.
+      'Class ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n ' +
 
-    // An internal member for keeping track of the
-    // transport for which this wrapper exists.
-    'Private m_Transport\n' +
+      // An internal member for keeping track of the
+      // transport for which this wrapper exists.
+      'Private m_Transport\n' +
 
-    // An internal member for keeping track of the
-    // auth token associated with the context that
-    // created this wrapper. Used for validation
-    // purposes.
-    'Private m_Auth\n' +
+      // An internal member for keeping track of the
+      // auth token associated with the context that
+      // created this wrapper. Used for validation
+      // purposes.
+      'Private m_Auth\n' +
 
-    // Method for internally setting the value
-    // of the m_Transport property. We have the
-    // isEmpty check to prevent the transport
-    // from being overridden with an illicit
-    // object by a malicious party.
-    'Public Sub SetTransport(transport)\n' +
-    'If isEmpty(m_Transport) Then\n' +
-    'Set m_Transport = transport\n' +
-    'End If\n' +
-    'End Sub\n' +
+      // Method for internally setting the value
+      // of the m_Transport property. We have the
+      // isEmpty check to prevent the transport
+      // from being overridden with an illicit
+      // object by a malicious party.
+      'Public Sub SetTransport(transport)\n' +
+      'If isEmpty(m_Transport) Then\n' +
+      'Set m_Transport = transport\n' +
+      'End If\n' +
+      'End Sub\n' +
 
-    // Method for internally setting the value
-    // of the m_Auth property. We have the
-    // isEmpty check to prevent the transport
-    // from being overridden with an illicit
-    // object by a malicious party.
-    'Public Sub SetAuth(auth)\n' +
-    'If isEmpty(m_Auth) Then\n' +
-    'm_Auth = auth\n' +
-    'End If\n' +
-    'End Sub\n' +
+      // Method for internally setting the value
+      // of the m_Auth property. We have the
+      // isEmpty check to prevent the transport
+      // from being overridden with an illicit
+      // object by a malicious party.
+      'Public Sub SetAuth(auth)\n' +
+      'If isEmpty(m_Auth) Then\n' +
+      'm_Auth = auth\n' +
+      'End If\n' +
+      'End Sub\n' +
 
-    // Returns the auth token to the gadget, so it can
-    // confirm a match before initiating the connection
-    'Public Function GetAuthToken()\n ' +
-    'GetAuthToken = m_Auth\n' +
-    'End Function\n' +
+      // Returns the auth token to the gadget, so it can
+      // confirm a match before initiating the connection
+      'Public Function GetAuthToken()\n ' +
+      'GetAuthToken = m_Auth\n' +
+      'End Function\n' +
 
-    // A wrapper method which causes a
-    // message to be sent to the other context.
-    'Public Sub SendMessage(service, payload)\n ' +
-    'Call m_Transport.' +
-    goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE + '(service, payload)\n' +
-    'End Sub\n' +
+      // A wrapper method which causes a
+      // message to be sent to the other context.
+      'Public Sub SendMessage(service, payload)\n ' +
+      'Call m_Transport.' +
+      goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE + '(service, payload)\n' +
+      'End Sub\n' +
 
-    // Method for setting up the inner->outer
-    // channel.
-    'Public Sub CreateChannel(channel)\n ' +
-    'Call m_Transport.' +
-    goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL + '(channel)\n' +
-    'End Sub\n' +
+      // Method for setting up the inner->outer
+      // channel.
+      'Public Sub CreateChannel(channel)\n ' +
+      'Call m_Transport.' +
+      goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL + '(channel)\n' +
+      'End Sub\n' +
 
-    // An empty field with a unique identifier to
-    // prevent the code from confusing this wrapper
-    // with a run-of-the-mill value found in window.opener.
-    'Public Sub ' + goog.net.xpc.NixTransport.NIX_ID_FIELD + '()\n ' +
-    'End Sub\n' +
-    'End Class\n ' +
+      // An empty field with a unique identifier to
+      // prevent the code from confusing this wrapper
+      // with a run-of-the-mill value found in window.opener.
+      'Public Sub ' + goog.net.xpc.NixTransport.NIX_ID_FIELD + '()\n ' +
+      'End Sub\n' +
+      'End Class\n ' +
 
-    // Function to get a reference to the wrapper.
-    'Function ' +
-    goog.net.xpc.NixTransport.NIX_GET_WRAPPER + '(transport, auth)\n' +
-    'Dim wrap\n' +
-    'Set wrap = New ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n' +
-    'wrap.SetTransport transport\n' +
-    'wrap.SetAuth auth\n' +
-    'Set ' + goog.net.xpc.NixTransport.NIX_GET_WRAPPER + ' = wrap\n' +
-    'End Function';
+      // Function to get a reference to the wrapper.
+      'Function ' +
+      goog.net.xpc.NixTransport.NIX_GET_WRAPPER + '(transport, auth)\n' +
+      'Dim wrap\n' +
+      'Set wrap = New ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n' +
+      'wrap.SetTransport transport\n' +
+      'wrap.SetAuth auth\n' +
+      'Set ' + goog.net.xpc.NixTransport.NIX_GET_WRAPPER + ' = wrap\n' +
+      'End Function';
 
   try {
     listenWindow.execScript(vbscript, 'vbscript');
@@ -266,7 +268,7 @@
  * @override
  */
 goog.net.xpc.NixTransport.prototype.transportType =
-   goog.net.xpc.TransportTypes.NIX;
+    goog.net.xpc.TransportTypes.NIX;
 
 
 /**
@@ -321,14 +323,14 @@
 
   // Get shortcut to iframe-element that contains the inner
   // page.
-  var innerFrame = this.channel_.iframeElement_;
+  var innerFrame = this.channel_.getIframeElement();
 
   try {
     // Attempt to place the NIX wrapper object into the inner
     // frame's opener property.
-    innerFrame.contentWindow.opener =
-      this.getWindow()[goog.net.xpc.NixTransport.NIX_GET_WRAPPER]
-        (this, this.authToken_);
+    var theWindow = this.getWindow();
+    var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
+    innerFrame.contentWindow.opener = getWrapper(this, this.authToken_);
     this.localSetupCompleted_ = true;
   }
   catch (e) {
@@ -378,9 +380,9 @@
 
       // Complete the construction of the channel by sending our own
       // wrapper to the container via the channel they gave us.
-      this.nixChannel_['CreateChannel'](
-        this.getWindow()[goog.net.xpc.NixTransport.NIX_GET_WRAPPER](this,
-                                                          this.authToken_));
+      var theWindow = this.getWindow();
+      var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
+      this.nixChannel_['CreateChannel'](getWrapper(this, this.authToken_));
 
       this.localSetupCompleted_ = true;
 
@@ -410,25 +412,25 @@
  * @private
  */
 goog.net.xpc.NixTransport.prototype.createChannel_ = function(channel) {
-   // Verify that the channel is in fact a NIX wrapper.
-   if (typeof channel != 'unknown' ||
-       !(goog.net.xpc.NixTransport.NIX_ID_FIELD in channel)) {
-     goog.net.xpc.logger.severe('Invalid NIX channel given to createChannel_');
-   }
+  // Verify that the channel is in fact a NIX wrapper.
+  if (typeof channel != 'unknown' ||
+      !(goog.net.xpc.NixTransport.NIX_ID_FIELD in channel)) {
+    goog.net.xpc.logger.severe('Invalid NIX channel given to createChannel_');
+  }
 
-   this.nixChannel_ = channel;
+  this.nixChannel_ = channel;
 
-   // Ensure that the NIX channel given to use is valid.
-   var remoteAuthToken = this.nixChannel_['GetAuthToken']();
+  // Ensure that the NIX channel given to use is valid.
+  var remoteAuthToken = this.nixChannel_['GetAuthToken']();
 
-   if (remoteAuthToken != this.remoteAuthToken_) {
-     goog.net.xpc.logger.severe('Invalid auth token from other party');
-     return;
-   }
+  if (remoteAuthToken != this.remoteAuthToken_) {
+    goog.net.xpc.logger.severe('Invalid auth token from other party');
+    return;
+  }
 
-   // Indicate to the CrossPageChannel that the channel is setup
-   // and ready to use.
-   this.channel_.notifyConnected();
+  // Indicate to the CrossPageChannel that the channel is setup
+  // and ready to use.
+  this.channel_.notifyConnected();
 };
 
 
@@ -444,7 +446,7 @@
     function(serviceName, payload) {
   /** @this {goog.net.xpc.NixTransport} */
   var deliveryHandler = function() {
-    this.channel_.safeDeliver(serviceName, payload);
+    this.channel_.xpcDeliver(serviceName, payload);
   };
   this.getWindow().setTimeout(goog.bind(deliveryHandler, this), 1);
 };
diff --git a/third_party/closure/goog/net/xpc/xpc.js b/third_party/closure/goog/net/xpc/xpc.js
index edd2ac0..be1b62e 100644
--- a/third_party/closure/goog/net/xpc/xpc.js
+++ b/third_party/closure/goog/net/xpc/xpc.js
@@ -252,10 +252,11 @@
 
 /**
  * Object holding active channels.
- * @type {Object}
- * @private
+ * Package private. Do not call from outside goog.net.xpc.
+ *
+ * @type {Object.<string, goog.net.xpc.CrossPageChannel>}
  */
-goog.net.xpc.channels_ = {};
+goog.net.xpc.channels = {};
 
 
 /**
diff --git a/third_party/closure/goog/object/object.js b/third_party/closure/goog/object/object.js
index 66d3dad..b20cf86 100644
--- a/third_party/closure/goog/object/object.js
+++ b/third_party/closure/goog/object/object.js
@@ -22,11 +22,12 @@
 /**
  * Calls a function for each element in an object/map/hash.
  *
- * @param {Object} obj The object over which to iterate.
- * @param {Function} f The function to call for every element. This function
- *     takes 3 arguments (the element, the index and the object)
- *     and the return value is irrelevant.
- * @param {Object=} opt_obj This is used as the 'this' object within f.
+ * @param {Object.<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object.<K,V>):?} f The function to call
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the object) and the return value is ignored.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @template T,K,V
  */
 goog.object.forEach = function(obj, f, opt_obj) {
   for (var key in obj) {
@@ -39,15 +40,17 @@
  * Calls a function for each element in an object/map/hash. If that call returns
  * true, adds the element to a new object.
  *
- * @param {Object} obj The object over which to iterate.
- * @param {Function} f The function to call for every element. This
+ * @param {Object.<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,K,?,Object.<K,V>):boolean} f The function to call
+ *     for every element. This
  *     function takes 3 arguments (the element, the index and the object)
  *     and should return a boolean. If the return value is true the
  *     element is added to the result object. If it is false the
  *     element is not included.
- * @param {Object=} opt_obj This is used as the 'this' object within f.
- * @return {!Object} a new object in which only elements that passed the test
- *     are present.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object.<K,V>} a new object in which only elements that passed the
+ *     test are present.
+ * @template T,K,V
  */
 goog.object.filter = function(obj, f, opt_obj) {
   var res = {};
@@ -64,13 +67,15 @@
  * For every element in an object/map/hash calls a function and inserts the
  * result into a new object.
  *
- * @param {Object} obj The object over which to iterate.
- * @param {Function} f The function to call for every element. This function
+ * @param {Object.<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object.<K,V>):R} f The function to call
+ *     for every element. This function
  *     takes 3 arguments (the element, the index and the object)
  *     and should return something. The result will be inserted
  *     into a new object.
- * @param {Object=} opt_obj This is used as the 'this' object within f.
- * @return {!Object} a new object with the results from f.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object.<T,R>} a new object with the results from f.
+ * @template T,K,V,R
  */
 goog.object.map = function(obj, f, opt_obj) {
   var res = {};
@@ -86,12 +91,14 @@
  * call returns true, returns true (without checking the rest). If
  * all calls return false, returns false.
  *
- * @param {Object} obj The object to check.
- * @param {Function} f The function to call for every element. This function
+ * @param {Object.<K,V>} obj The object to check.
+ * @param {function(this:T,K,?,Object.<K,V>):boolean} f The function to
+ *     call for every element. This function
  *     takes 3 arguments (the element, the index and the object) and should
  *     return a boolean.
- * @param {Object=} opt_obj This is used as the 'this' object within f.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
  * @return {boolean} true if any element passes the test.
+ * @template T,K,V
  */
 goog.object.some = function(obj, f, opt_obj) {
   for (var key in obj) {
@@ -108,12 +115,14 @@
  * all calls return true, returns true. If any call returns false, returns
  * false at this point and does not continue to check the remaining elements.
  *
- * @param {Object} obj The object to check.
- * @param {Function} f The function to call for every element. This function
+ * @param {Object.<K,V>} obj The object to check.
+ * @param {?function(this:T,V,?,Object.<K,V>):boolean} f The function to
+ *     call for every element. This function
  *     takes 3 arguments (the element, the index and the object) and should
  *     return a boolean.
- * @param {Object=} opt_obj This is used as the 'this' object within f.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
  * @return {boolean} false if any element fails the test.
+ * @template T,K,V
  */
 goog.object.every = function(obj, f, opt_obj) {
   for (var key in obj) {
@@ -164,8 +173,9 @@
  * For map literals the returned value will be the first one in most of the
  * browsers (a know exception is Konqueror).
  *
- * @param {Object} obj The object to pick a value from.
- * @return {*} The value or undefined if the object is empty.
+ * @param {Object.<K,V>} obj The object to pick a value from.
+ * @return {V|undefined} The value or undefined if the object is empty.
+ * @template K,V
  */
 goog.object.getAnyValue = function(obj) {
   for (var key in obj) {
@@ -178,9 +188,10 @@
  * Whether the object/hash/map contains the given object as a value.
  * An alias for goog.object.containsValue(obj, val).
  *
- * @param {Object} obj The object in which to look for val.
- * @param {*} val The object for which to check.
+ * @param {Object.<K,V>} obj The object in which to look for val.
+ * @param {V} val The object for which to check.
  * @return {boolean} true if val is present.
+ * @template K,V
  */
 goog.object.contains = function(obj, val) {
   return goog.object.containsValue(obj, val);
@@ -190,8 +201,9 @@
 /**
  * Returns the values of the object/map/hash.
  *
- * @param {Object} obj The object from which to get the values.
- * @return {!Array} The values in the object/map/hash.
+ * @param {Object.<K,V>} obj The object from which to get the values.
+ * @return {!Array.<V>} The values in the object/map/hash.
+ * @template K,V
  */
 goog.object.getValues = function(obj) {
   var res = [];
@@ -262,9 +274,10 @@
 /**
  * Whether the object/map/hash contains the given value. This is O(n).
  *
- * @param {Object} obj The object in which to look for val.
- * @param {*} val The value for which to check.
+ * @param {Object.<K,V>} obj The object in which to look for val.
+ * @param {V} val The value for which to check.
  * @return {boolean} true If the map contains the value.
+ * @template K,V
  */
 goog.object.containsValue = function(obj, val) {
   for (var key in obj) {
@@ -279,13 +292,14 @@
 /**
  * Searches an object for an element that satisfies the given condition and
  * returns its key.
- * @param {Object} obj The object to search in.
- * @param {function(*, string, Object): boolean} f The function to call for
- *     every element. Takes 3 arguments (the value, the key and the object) and
- *     should return a boolean.
- * @param {Object=} opt_this An optional "this" context for the function.
+ * @param {Object.<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object.<K,V>):boolean} f The
+ *      function to call for every element. Takes 3 arguments (the value,
+ *     the key and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
  * @return {string|undefined} The key of an element for which the function
  *     returns true or undefined if no such element is found.
+ * @template T,K,V
  */
 goog.object.findKey = function(obj, f, opt_this) {
   for (var key in obj) {
@@ -300,13 +314,14 @@
 /**
  * Searches an object for an element that satisfies the given condition and
  * returns its value.
- * @param {Object} obj The object to search in.
- * @param {function(*, string, Object): boolean} f The function to call for
- *     every element. Takes 3 arguments (the value, the key and the object) and
- *     should return a boolean.
- * @param {Object=} opt_this An optional "this" context for the function.
- * @return {*} The value of an element for which the function returns true or
+ * @param {Object.<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object.<K,V>):boolean} f The function
+ *     to call for every element. Takes 3 arguments (the value, the key
+ *     and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
+ * @return {V} The value of an element for which the function returns true or
  *     undefined if no such element is found.
+ * @template T,K,V
  */
 goog.object.findValue = function(obj, f, opt_this) {
   var key = goog.object.findKey(obj, f, opt_this);
@@ -360,9 +375,10 @@
  * Adds a key-value pair to the object. Throws an exception if the key is
  * already in use. Use set if you want to change an existing pair.
  *
- * @param {Object} obj The object to which to add the key-value pair.
+ * @param {Object.<K,V>} obj The object to which to add the key-value pair.
  * @param {string} key The key to add.
- * @param {*} val The value to add.
+ * @param {V} val The value to add.
+ * @template K,V
  */
 goog.object.add = function(obj, key, val) {
   if (key in obj) {
@@ -375,11 +391,12 @@
 /**
  * Returns the value for the given key.
  *
- * @param {Object} obj The object from which to get the value.
+ * @param {Object.<K,V>} obj The object from which to get the value.
  * @param {string} key The key for which to get the value.
- * @param {*=} opt_val The value to return if no item is found for the given
+ * @param {R=} opt_val The value to return if no item is found for the given
  *     key (default is undefined).
- * @return {*} The value for the given key.
+ * @return {V|R|undefined} The value for the given key.
+ * @template K,V,R
  */
 goog.object.get = function(obj, key, opt_val) {
   if (key in obj) {
@@ -392,9 +409,10 @@
 /**
  * Adds a key-value pair to the object/map/hash.
  *
- * @param {Object} obj The object to which to add the key-value pair.
+ * @param {Object.<K,V>} obj The object to which to add the key-value pair.
  * @param {string} key The key to add.
- * @param {*} value The value to add.
+ * @param {K} value The value to add.
+ * @template K,V
  */
 goog.object.set = function(obj, key, value) {
   obj[key] = value;
@@ -404,10 +422,11 @@
 /**
  * Adds a key-value pair to the object/map/hash if it doesn't exist yet.
  *
- * @param {Object} obj The object to which to add the key-value pair.
+ * @param {Object.<K,V>} obj The object to which to add the key-value pair.
  * @param {string} key The key to add.
- * @param {*} value The value to add if the key wasn't present.
- * @return {*} The value of the entry at the end of the function.
+ * @param {V} value The value to add if the key wasn't present.
+ * @return {V} The value of the entry at the end of the function.
+ * @template K,V
  */
 goog.object.setIfUndefined = function(obj, key, value) {
   return key in obj ? obj[key] : (obj[key] = value);
@@ -417,8 +436,9 @@
 /**
  * Does a flat clone of the object.
  *
- * @param {Object} obj Object to clone.
- * @return {!Object} Clone of the input object.
+ * @param {Object.<K,V>} obj Object to clone.
+ * @return {!Object.<K,V>} Clone of the input object.
+ * @template K,V
  */
 goog.object.clone = function(obj) {
   // We cannot use the prototype trick because a lot of methods depend on where
@@ -591,9 +611,10 @@
  * In default mode, writes to this view will fail silently. In strict mode,
  * they will throw an error.
  *
- * @param {!Object} obj An object.
- * @return {!Object} An immutable view of that object, or the original object
- *     if this browser does not support immutables.
+ * @param {!Object.<K,V>} obj An object.
+ * @return {!Object.<K,V>} An immutable view of that object, or the
+ *     original object if this browser does not support immutables.
+ * @template K,V
  */
 goog.object.createImmutableView = function(obj) {
   var result = obj;
diff --git a/third_party/closure/goog/proto2/fielddescriptor.js b/third_party/closure/goog/proto2/fielddescriptor.js
index 77114f1..84c3abd 100644
--- a/third_party/closure/goog/proto2/fielddescriptor.js
+++ b/third_party/closure/goog/proto2/fielddescriptor.js
@@ -171,7 +171,7 @@
  * @return {goog.proto2.Descriptor} The descriptor.
  */
 goog.proto2.FieldDescriptor.prototype.getContainingType = function() {
-  return this.parent_.descriptor_;
+  return this.parent_.getDescriptor();
 };
 
 
@@ -250,7 +250,7 @@
 goog.proto2.FieldDescriptor.prototype.getFieldMessageType = function() {
   goog.proto2.Util.assert(this.isCompositeType(), 'Expected message or group');
 
-  return this.nativeType_.descriptor_;
+  return this.nativeType_.getDescriptor();
 };
 
 
diff --git a/third_party/closure/goog/proto2/message.js b/third_party/closure/goog/proto2/message.js
index 86532d6..c0def32 100644
--- a/third_party/closure/goog/proto2/message.js
+++ b/third_party/closure/goog/proto2/message.js
@@ -39,22 +39,12 @@
    */
   this.values_ = {};
 
-  // The descriptor_ is static to the message function that is being created.
-  // Therefore, we retrieve it via the constructor.
-
-  /**
-   * Stores the information (i.e. metadata) about this message.
-   * @type {!goog.proto2.Descriptor}
-   * @private
-   */
-  this.descriptor_ = this.constructor.descriptor_;
-
   /**
    * Stores the field information (i.e. metadata) about this message.
    * @type {Object.<number, !goog.proto2.FieldDescriptor>}
    * @private
    */
-  this.fields_ = this.descriptor_.getFieldsMap();
+  this.fields_ = this.getDescriptor().getFieldsMap();
 
   /**
    * The lazy deserializer for this message instance, if any.
@@ -109,6 +99,34 @@
 
 
 /**
+ * All instances of goog.proto2.Message should have a static descriptorObj_
+ * property. This is a JSON representation of a Descriptor. The real Descriptor
+ * will be deserialized lazily in the getDescriptor() method.
+ *
+ * This declaration is just here for documentation purposes.
+ * goog.proto2.Message does not have its own descriptor.
+ *
+ * @type {undefined}
+ * @private
+ */
+goog.proto2.Message.descriptorObj_;
+
+
+/**
+ * All instances of goog.proto2.Message should have a static descriptor_
+ * property. The Descriptor will be deserialized lazily in the getDescriptor()
+ * method.
+ *
+ * This declaration is just here for documentation purposes.
+ * goog.proto2.Message does not have its own descriptor.
+ *
+ * @type {undefined}
+ * @private
+ */
+goog.proto2.Message.descriptor_;
+
+
+/**
  * Initializes the message with a lazy deserializer and its associated data.
  * This method should be called by internal methods ONLY.
  *
@@ -167,10 +185,18 @@
 /**
  * Returns the descriptor which describes the current message.
  *
- * @return {goog.proto2.Descriptor} The descriptor.
+ * This only works if we assume people never subclass protobufs.
+ *
+ * @return {!goog.proto2.Descriptor} The descriptor.
  */
 goog.proto2.Message.prototype.getDescriptor = function() {
-  return this.descriptor_;
+  // NOTE(nicksantos): These sorts of indirect references to descriptor
+  // through this.constructor are fragile. See the comments
+  // in set$Metadata for more info.
+  var Ctor = this.constructor;
+  return Ctor.descriptor_ ||
+      (Ctor.descriptor_ = goog.proto2.Message.create$Descriptor(
+           Ctor, Ctor.descriptorObj_));
 };
 
 
@@ -185,7 +211,7 @@
  */
 goog.proto2.Message.prototype.has = function(field) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   return this.has$Value(field.getTag());
@@ -202,7 +228,7 @@
  */
 goog.proto2.Message.prototype.arrayOf = function(field) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   return this.array$Values(field.getTag());
@@ -219,7 +245,7 @@
  */
 goog.proto2.Message.prototype.countOf = function(field) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   return this.count$Values(field.getTag());
@@ -239,7 +265,7 @@
  */
 goog.proto2.Message.prototype.get = function(field, opt_index) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   return this.get$Value(field.getTag(), opt_index);
@@ -259,7 +285,7 @@
  */
 goog.proto2.Message.prototype.getOrDefault = function(field, opt_index) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   return this.get$ValueOrDefault(field.getTag(), opt_index);
@@ -276,7 +302,7 @@
  */
 goog.proto2.Message.prototype.set = function(field, value) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   this.set$Value(field.getTag(), value);
@@ -293,7 +319,7 @@
  */
 goog.proto2.Message.prototype.add = function(field, value) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   this.add$Value(field.getTag(), value);
@@ -307,7 +333,7 @@
  */
 goog.proto2.Message.prototype.clear = function(field) {
   goog.proto2.Util.assert(
-      field.getContainingType() == this.descriptor_,
+      field.getContainingType() == this.getDescriptor(),
       'The current message does not contain the given field');
 
   this.clear$Field(field.getTag());
@@ -798,13 +824,14 @@
  * @param {Object} metadataObj The object containing the metadata.
  */
 goog.proto2.Message.set$Metadata = function(messageType, metadataObj) {
-  // TODO(nicksantos): Change the code generator so that it doesn't
-  // alias the message constructor. Then it will be easier for the compiler
-  // to devirtualize these symbols.
-  messageType.descriptor_ = goog.proto2.Message.create$Descriptor(
-      /** @type {function(new:goog.proto2.Message)} */ (messageType),
-      metadataObj);
+  // NOTE(nicksantos): JSCompiler's type-based optimizations really do not
+  // like indirectly defined methods (both prototype methods and
+  // static methods). This is very fragile in compiled code. I think it only
+  // really works by accident, and is highly likely to break in the future.
+  messageType.descriptorObj_ = metadataObj;
   messageType.getDescriptor = function() {
-    return messageType.descriptor_;
+    // The descriptor is created lazily when we instantiate a new instance.
+    return messageType.descriptor_ ||
+        (new messageType()).getDescriptor();
   };
 };
diff --git a/third_party/closure/goog/string/string.js b/third_party/closure/goog/string/string.js
index 7875a2b..ee4aafc 100644
--- a/third_party/closure/goog/string/string.js
+++ b/third_party/closure/goog/string/string.js
@@ -136,9 +136,10 @@
 
 
 /**
- * Checks if a string is null, empty or contains only whitespaces.
+ * Checks if a string is null, undefined, empty or contains only whitespaces.
  * @param {*} str The string to check.
- * @return {boolean} True if{@code str} is null, empty, or whitespace only.
+ * @return {boolean} True if{@code str} is null, undefined, empty, or
+ *     whitespace only.
  */
 goog.string.isEmptySafe = function(str) {
   return goog.string.isEmpty(goog.string.makeSafe(str));
diff --git a/third_party/closure/goog/structs/structs.js b/third_party/closure/goog/structs/structs.js
index ec39582..c5b19c5 100644
--- a/third_party/closure/goog/structs/structs.js
+++ b/third_party/closure/goog/structs/structs.js
@@ -161,12 +161,14 @@
  * Calls a function for each value in a collection. The function takes
  * three arguments; the value, the key and the collection.
  *
- * @param {Object} col The collection-like object.
- * @param {Function} f The function to call for every value. This function takes
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):?} f The function to call for every value.
+ *     This function takes
  *     3 arguments (the value, the key or undefined if the collection has no
  *     notion of keys, and the collection) and the return value is irrelevant.
- * @param {Object=} opt_obj The object to be used as the value of 'this'
+ * @param {T=} opt_obj The object to be used as the value of 'this'
  *     within {@code f}.
+ * @template T,S
  */
 goog.structs.forEach = function(col, f, opt_obj) {
   if (typeof col.forEach == 'function') {
@@ -188,17 +190,19 @@
  * Calls a function for every value in the collection. When a call returns true,
  * adds the value to a new collection (Array is returned by default).
  *
- * @param {Object} col The collection-like object.
- * @param {Function} f The function to call for every value. This function takes
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes
  *     3 arguments (the value, the key or undefined if the collection has no
  *     notion of keys, and the collection) and should return a Boolean. If the
  *     return value is true the value is added to the result collection. If it
  *     is false the value is not included.
- * @param {Object=} opt_obj The object to be used as the value of 'this'
+ * @param {T=} opt_obj The object to be used as the value of 'this'
  *     within {@code f}.
  * @return {!Object|!Array} A new collection where the passed values are
  *     present. If col is a key-less collection an array is returned.  If col
  *     has keys and values a plain old JS object is returned.
+ * @template T,S
  */
 goog.structs.filter = function(col, f, opt_obj) {
   if (typeof col.filter == 'function') {
@@ -238,16 +242,17 @@
  * Calls a function for every value in the collection and adds the result into a
  * new collection (defaults to creating a new Array).
  *
- * @param {Object} col The collection-like object.
- * @param {Function} f The function to call for every value. This function
- *     takes 3 arguments (the value, the key or undefined if the collection has
- *     no notion of keys, and the collection) and should return something. The
- *     result will be used as the value in the new collection.
- * @param {Object=} opt_obj  The object to be used as the value of 'this'
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):V} f The function to call for every value.
+ *     This function takes 3 arguments (the value, the key or undefined if the
+ *     collection has no notion of keys, and the collection) and should return
+ *     something. The result will be used as the value in the new collection.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
  *     within {@code f}.
- * @return {!Object|!Array} A new collection with the new values.  If col is a
- *     key-less collection an array is returned.  If col has keys and values a
- *     plain old JS object is returned.
+ * @return {!Object.<V>|!Array.<V>} A new collection with the new values.  If
+ *     col is a key-less collection an array is returned.  If col has keys and
+ *     values a plain old JS object is returned.
+ * @template T,S,V
  */
 goog.structs.map = function(col, f, opt_obj) {
   if (typeof col.map == 'function') {
@@ -283,13 +288,15 @@
  * Calls f for each value in a collection. If any call returns true this returns
  * true (without checking the rest). If all returns false this returns false.
  *
- * @param {Object|Array|string} col The collection-like object.
- * @param {Function} f The function to call for every value. This function takes
- *     3 arguments (the value, the key or undefined if the collection has no
- *     notion of keys, and the collection) and should return a Boolean.
- * @param {Object=} opt_obj  The object to be used as the value of 'this'
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or undefined
+ *     if the collection has no notion of keys, and the collection) and should
+ *     return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
  *     within {@code f}.
  * @return {boolean} True if any value passes the test.
+ * @template T,S
  */
 goog.structs.some = function(col, f, opt_obj) {
   if (typeof col.some == 'function') {
@@ -315,13 +322,15 @@
  * true this returns true. If any returns false this returns false at this point
  *  and does not continue to check the remaining values.
  *
- * @param {Object} col The collection-like object.
- * @param {Function} f The function to call for every value. This function takes
- *     3 arguments (the value, the key or undefined if the collection has no
- *     notion of keys, and the collection) and should return a Boolean.
- * @param {Object=} opt_obj  The object to be used as the value of 'this'
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or
+ *     undefined if the collection has no notion of keys, and the collection)
+ *     and should return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
  *     within {@code f}.
  * @return {boolean} True if all key-value pairs pass the test.
+ * @template T,S
  */
 goog.structs.every = function(col, f, opt_obj) {
   if (typeof col.every == 'function') {
diff --git a/third_party/closure/goog/style/transition.js b/third_party/closure/goog/style/transition.js
index a0e13d0..fe05ecc 100644
--- a/third_party/closure/goog/style/transition.js
+++ b/third_party/closure/goog/style/transition.js
@@ -62,10 +62,14 @@
         if (goog.isString(p)) {
           return p;
         } else {
-          goog.asserts.assert(p && p.property && goog.isNumber(p.duration) &&
-              p.timing && goog.isNumber(p.delay));
-          return p.property + ' ' + p.duration + 's ' + p.timing + ' ' +
-              p.delay + 's';
+          goog.asserts.assertObject(p,
+              'Expected css3 property to be an object.');
+          var propString = p.property + ' ' + p.duration + 's ' + p.timing +
+              ' ' + p.delay + 's';
+          goog.asserts.assert(p.property && goog.isNumber(p.duration) &&
+              p.timing && goog.isNumber(p.delay),
+              'Unexpected css3 property value: %s', propString);
+          return propString;
         }
       });
   goog.style.transition.setPropertyValue_(element, values.join(','));
diff --git a/third_party/closure/goog/testing/asserts.js b/third_party/closure/goog/testing/asserts.js
index feff5cc..bdf7b19 100644
--- a/third_party/closure/goog/testing/asserts.js
+++ b/third_party/closure/goog/testing/asserts.js
@@ -141,7 +141,7 @@
 };
 
 var fail = function(failureMessage) {
-  goog.testing.asserts.raiseException_('Call to fail()', failureMessage);
+  goog.testing.asserts.raiseException('Call to fail()', failureMessage);
 };
 
 var argumentsIncludeComments = function(expectedNumberOfNonCommentArgs, args) {
@@ -172,7 +172,7 @@
 
 var _assert = function(comment, booleanValue, failureMessage) {
   if (!booleanValue) {
-    goog.testing.asserts.raiseException_(comment, failureMessage);
+    goog.testing.asserts.raiseException(comment, failureMessage);
   }
 };
 
@@ -267,7 +267,7 @@
     }
     return e;
   }
-  goog.testing.asserts.raiseException_(comment,
+  goog.testing.asserts.raiseException(comment,
       'No exception thrown from function passed to assertThrows');
 };
 
@@ -296,7 +296,7 @@
     // Some browsers don't have a stack trace so at least have the error
     // description.
     var stackTrace = e['stack'] || e['stacktrace'] || e.toString();
-    goog.testing.asserts.raiseException_(comment, stackTrace);
+    goog.testing.asserts.raiseException(comment, stackTrace);
   }
 };
 
@@ -1143,9 +1143,8 @@
  * Raises a JsUnit exception with the given comment.
  * @param {string} comment A summary for the exception.
  * @param {string=} opt_message A description of the exception.
- * @private
  */
-goog.testing.asserts.raiseException_ = function(comment, opt_message) {
+goog.testing.asserts.raiseException = function(comment, opt_message) {
   if (goog.global['CLOSURE_INSPECTOR___'] &&
       goog.global['CLOSURE_INSPECTOR___']['supportsJSUnit']) {
     goog.global['CLOSURE_INSPECTOR___']['jsUnitFailure'](comment, opt_message);
diff --git a/third_party/closure/goog/testing/dom.js b/third_party/closure/goog/testing/dom.js
index 1179a29..487fbb4 100644
--- a/third_party/closure/goog/testing/dom.js
+++ b/third_party/closure/goog/testing/dom.js
@@ -157,7 +157,7 @@
 /**
  * Map function that converts end tags to a specific object.
  * @param {Node} node The node to map.
- * @param {Object} ignore Always undefined.
+ * @param {undefined} ignore Always undefined.
  * @param {goog.dom.TagIterator} iterator The iterator.
  * @return {Node|Object} The resulting iteration item.
  * @private
diff --git a/third_party/closure/goog/testing/events/events.js b/third_party/closure/goog/testing/events/events.js
index fc7e578..4e9bf72 100644
--- a/third_party/closure/goog/testing/events/events.js
+++ b/third_party/closure/goog/testing/events/events.js
@@ -575,3 +575,84 @@
 
   return event.returnValue_;
 };
+
+
+/**
+ * Simulates a touchstart event on the given target.
+ * @param {EventTarget} target The target for the event.
+ * @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
+ *     target's position (if available), otherwise (0, 0).
+ * @param {Object=} opt_eventProperties Event properties to be mixed into the
+ *     BrowserEvent.
+ * @return {boolean} The returnValue of the event: false if preventDefault() was
+ *     called on it, true otherwise.
+ */
+goog.testing.events.fireTouchStartEvent = function(
+    target, opt_coords, opt_eventProperties) {
+  // TODO: Support multi-touch events with array of coordinates.
+  var touchstart =
+      new goog.testing.events.Event(goog.events.EventType.TOUCHSTART, target);
+  goog.testing.events.setEventClientXY_(touchstart, opt_coords);
+  return goog.testing.events.fireBrowserEvent(touchstart);
+};
+
+
+/**
+ * Simulates a touchmove event on the given target.
+ * @param {EventTarget} target The target for the event.
+ * @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
+ *     target's position (if available), otherwise (0, 0).
+ * @param {Object=} opt_eventProperties Event properties to be mixed into the
+ *     BrowserEvent.
+ * @return {boolean} The returnValue of the event: false if preventDefault() was
+ *     called on it, true otherwise.
+ */
+goog.testing.events.fireTouchMoveEvent = function(
+    target, opt_coords, opt_eventProperties) {
+  // TODO: Support multi-touch events with array of coordinates.
+  var touchmove =
+      new goog.testing.events.Event(goog.events.EventType.TOUCHMOVE, target);
+  goog.testing.events.setEventClientXY_(touchmove, opt_coords);
+  return goog.testing.events.fireBrowserEvent(touchmove);
+};
+
+
+/**
+ * Simulates a touchend event on the given target.
+ * @param {EventTarget} target The target for the event.
+ * @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
+ *     target's position (if available), otherwise (0, 0).
+ * @param {Object=} opt_eventProperties Event properties to be mixed into the
+ *     BrowserEvent.
+ * @return {boolean} The returnValue of the event: false if preventDefault() was
+ *     called on it, true otherwise.
+ */
+goog.testing.events.fireTouchEndEvent = function(
+    target, opt_coords, opt_eventProperties) {
+  // TODO: Support multi-touch events with array of coordinates.
+  var touchend =
+      new goog.testing.events.Event(goog.events.EventType.TOUCHEND, target);
+  goog.testing.events.setEventClientXY_(touchend, opt_coords);
+  return goog.testing.events.fireBrowserEvent(touchend);
+};
+
+
+/**
+ * Simulates a simple touch sequence on the given target.
+ * @param {EventTarget} target The target for the event.
+ * @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event
+ *     target's position (if available), otherwise (0, 0).
+ * @param {Object=} opt_eventProperties Event properties to be mixed into the
+ *     BrowserEvent.
+ * @return {boolean} The returnValue of the sequence: false if preventDefault()
+ *     was called on any of the events, true otherwise.
+ */
+goog.testing.events.fireTouchSequence = function(
+    target, opt_coords, opt_eventProperties) {
+  // TODO: Support multi-touch events with array of coordinates.
+  // Fire touchstart, touchmove, touchend then return the bitwise AND of the 3.
+  return !!(goog.testing.events.fireTouchStartEvent(
+                target, opt_coords, opt_eventProperties) &
+            goog.testing.events.fireTouchEndEvent(
+                target, opt_coords, opt_eventProperties));
+};
diff --git a/third_party/closure/goog/testing/net/xhrio.js b/third_party/closure/goog/testing/net/xhrio.js
index 6271c12..b59a1d5 100644
--- a/third_party/closure/goog/testing/net/xhrio.js
+++ b/third_party/closure/goog/testing/net/xhrio.js
@@ -63,6 +63,13 @@
 
 
 /**
+ * Alias this enum here to make mocking of goog.net.XhrIo easier.
+ * @enum {string}
+ */
+goog.testing.net.XhrIo.ResponseType = goog.net.XhrIo.ResponseType;
+
+
+/**
  * All non-disposed instances of goog.testing.net.XhrIo created
  * by {@link goog.testing.net.XhrIo.send} are in this Array.
  * @see goog.testing.net.XhrIo.cleanup
diff --git a/third_party/closure/goog/ui/ac/autocomplete.js b/third_party/closure/goog/ui/ac/autocomplete.js
index 5ce7570..46bcecf 100644
--- a/third_party/closure/goog/ui/ac/autocomplete.js
+++ b/third_party/closure/goog/ui/ac/autocomplete.js
@@ -509,6 +509,7 @@
   this.dismissTimer_ = null;
   this.renderer_.dismiss();
   this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
+  this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.DISMISS);
 };
 
 
diff --git a/third_party/closure/goog/ui/ac/renderer.js b/third_party/closure/goog/ui/ac/renderer.js
index fd90194..2473b37 100644
--- a/third_party/closure/goog/ui/ac/renderer.js
+++ b/third_party/closure/goog/ui/ac/renderer.js
@@ -282,6 +282,15 @@
 
 
 /**
+ * @return {boolean} Whether we should be aligning to the top of
+ *     the target element.
+ */
+goog.ui.ac.Renderer.prototype.getTopAlign = function() {
+  return this.topAlign_;
+};
+
+
+/**
  * Set whether to align autocomplete to the right of the target element.
  * @param {boolean} align If true, align to right.
  */
@@ -291,6 +300,14 @@
 
 
 /**
+ * @return {boolean} Whether the autocomplete menu should be right aligned.
+ */
+goog.ui.ac.Renderer.prototype.getRightAlign = function() {
+  return this.rightAlign_;
+};
+
+
+/**
  * Set whether or not standard highlighting should be used when rendering rows.
  * @param {boolean} useStandardHighlighting true if standard highlighting used.
  */
@@ -566,18 +583,28 @@
 
 
 /**
+ * @return {goog.positioning.Corner} The anchor corner to position the popup at.
+ * @protected
+ */
+goog.ui.ac.Renderer.prototype.getAnchorCorner = function() {
+  var anchorCorner = this.rightAlign_ ?
+      goog.positioning.Corner.BOTTOM_RIGHT :
+      goog.positioning.Corner.BOTTOM_LEFT;
+  if (this.topAlign_) {
+    anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
+  }
+  return anchorCorner;
+};
+
+
+/**
  * Repositions the auto complete popup relative to the location node, if it
  * exists and the auto position has been set.
  */
 goog.ui.ac.Renderer.prototype.reposition = function() {
   if (this.target_ && this.reposition_) {
     var anchorElement = this.anchorElement_ || this.target_;
-    var anchorCorner = this.rightAlign_ ?
-        goog.positioning.Corner.BOTTOM_RIGHT :
-        goog.positioning.Corner.BOTTOM_LEFT;
-    if (this.topAlign_) {
-      anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
-    }
+    var anchorCorner = this.getAnchorCorner();
 
     goog.positioning.positionAtAnchor(
         anchorElement, anchorCorner,
diff --git a/third_party/closure/goog/ui/activitymonitor.js b/third_party/closure/goog/ui/activitymonitor.js
index 136c999..a1557e7 100644
--- a/third_party/closure/goog/ui/activitymonitor.js
+++ b/third_party/closure/goog/ui/activitymonitor.js
@@ -142,7 +142,8 @@
 goog.ui.ActivityMonitor.userEventTypesBody_ =
   [goog.events.EventType.CLICK, goog.events.EventType.DBLCLICK,
    goog.events.EventType.MOUSEDOWN, goog.events.EventType.MOUSEUP,
-   goog.events.EventType.MOUSEMOVE];
+   goog.events.EventType.MOUSEMOVE, goog.events.EventType.TOUCHSTART,
+   goog.events.EventType.TOUCHMOVE, goog.events.EventType.TOUCHEND];
 
 
 /**
diff --git a/third_party/closure/goog/ui/component.js b/third_party/closure/goog/ui/component.js
index eab98dd..48a262f 100644
--- a/third_party/closure/goog/ui/component.js
+++ b/third_party/closure/goog/ui/component.js
@@ -1015,7 +1015,10 @@
     // render_() calls enterDocument() if the parent is already in the document.
     child.render_(this.getContentElement(), sibling ? sibling.element_ : null);
   } else if (this.inDocument_ && !child.inDocument_ && child.element_ &&
-      child.element_.parentNode) {
+      child.element_.parentNode &&
+      // Under some circumstances, IE8 implicitly creates a Document Fragment
+      // for detached nodes, so ensure the parent is an Element as it should be.
+      child.element_.parentNode.nodeType == goog.dom.NodeType.ELEMENT) {
     // We don't touch the DOM, but if the parent is in the document, and the
     // child element is in the document but not marked as such, then we call
     // enterDocument on the child.
@@ -1134,9 +1137,10 @@
  * {@code opt_obj} is provided, it will be used as the 'this' object in the
  * function when called.  The function should take two arguments:  the child
  * component and its 0-based index.  The return value is ignored.
- * @param {Function} f The function to call for every child component; should
- *    take 2 arguments (the child and its index).
- * @param {Object=} opt_obj Used as the 'this' object in f when called.
+ * @param {function(this:T,?,number):?} f The function to call for every
+ * child component; should take 2 arguments (the child and its index).
+ * @param {T=} opt_obj Used as the 'this' object in f when called.
+ * @template T
  */
 goog.ui.Component.prototype.forEachChild = function(f, opt_obj) {
   if (this.children_) {
@@ -1233,7 +1237,7 @@
  * @see goog.ui.Component#removeChild
  * @param {boolean=} opt_unrender If true, calls {@link #exitDocument} on the
  *    removed child components, and detaches their DOM from the document.
- * @return {!Array.<goog.ui.Component>|undefined} The removed components if any.
+ * @return {!Array.<goog.ui.Component>} The removed components if any.
  */
 goog.ui.Component.prototype.removeChildren = function(opt_unrender) {
   var removedChildren = [];
diff --git a/third_party/closure/goog/ui/container.js b/third_party/closure/goog/ui/container.js
index 4c71456..7fed245 100644
--- a/third_party/closure/goog/ui/container.js
+++ b/third_party/closure/goog/ui/container.js
@@ -41,7 +41,7 @@
 goog.require('goog.ui.Component.EventType');
 goog.require('goog.ui.Component.State');
 goog.require('goog.ui.ContainerRenderer');
-
+goog.require('goog.ui.Control');
 
 
 /**
@@ -833,6 +833,8 @@
  * @override
  */
 goog.ui.Container.prototype.addChild = function(child, opt_render) {
+  goog.asserts.assertInstanceof(child, goog.ui.Control,
+      'The child of a container must be a control');
   goog.ui.Container.superClass_.addChild.call(this, child, opt_render);
 };
 
diff --git a/third_party/closure/goog/ui/editor/bubble.js b/third_party/closure/goog/ui/editor/bubble.js
index 6e93888..a0a2744 100644
--- a/third_party/closure/goog/ui/editor/bubble.js
+++ b/third_party/closure/goog/ui/editor/bubble.js
@@ -176,7 +176,6 @@
 
 /**
  * @return {Element} The element that where the bubble's contents go.
- * @protected
  */
 goog.ui.editor.Bubble.prototype.getContentElement = function() {
   return this.bubbleContents_;
diff --git a/third_party/closure/goog/ui/menubutton.js b/third_party/closure/goog/ui/menubutton.js
index 5d0f068..2069528 100644
--- a/third_party/closure/goog/ui/menubutton.js
+++ b/third_party/closure/goog/ui/menubutton.js
@@ -328,8 +328,9 @@
 
   if (e.keyCode == goog.events.KeyCodes.DOWN ||
       e.keyCode == goog.events.KeyCodes.UP ||
-      e.keyCode == goog.events.KeyCodes.SPACE) {
-    // Menu is closed, and the user hit the down/up/space key; open menu.
+      e.keyCode == goog.events.KeyCodes.SPACE ||
+      e.keyCode == goog.events.KeyCodes.ENTER) {
+    // Menu is closed, and the user hit the down/up/space/enter key; open menu.
     this.setOpen(true);
     return true;
   }
diff --git a/third_party/closure/goog/ui/palette.js b/third_party/closure/goog/ui/palette.js
index 85f3a02..3cc6a07 100644
--- a/third_party/closure/goog/ui/palette.js
+++ b/third_party/closure/goog/ui/palette.js
@@ -27,8 +27,7 @@
 goog.require('goog.events.EventType');
 goog.require('goog.events.KeyCodes');
 goog.require('goog.math.Size');
-goog.require('goog.ui.Component.Error');
-goog.require('goog.ui.Component.EventType');
+goog.require('goog.ui.Component');
 goog.require('goog.ui.Control');
 goog.require('goog.ui.PaletteRenderer');
 goog.require('goog.ui.SelectionModel');
@@ -55,8 +54,10 @@
  * @extends {goog.ui.Control}
  */
 goog.ui.Palette = function(items, opt_renderer, opt_domHelper) {
-  goog.ui.Control.call(this, items,
+  goog.base(this, items,
       opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);
+  this.setAutoStates(goog.ui.Component.State.CHECKED |
+      goog.ui.Component.State.SELECTED | goog.ui.Component.State.OPENED, false);
 };
 goog.inherits(goog.ui.Palette, goog.ui.Control);
 
@@ -237,7 +238,7 @@
   var item = this.getHighlightedItem();
   if (item) {
     this.setSelectedItem(item);
-    return this.dispatchEvent(goog.ui.Component.EventType.ACTION);
+    return goog.base(this, 'performActionInternal', e);
   }
   return false;
 };
diff --git a/third_party/closure/goog/ui/splitpane.js b/third_party/closure/goog/ui/splitpane.js
index d36b1f3..f4a480d 100644
--- a/third_party/closure/goog/ui/splitpane.js
+++ b/third_party/closure/goog/ui/splitpane.js
@@ -64,7 +64,7 @@
  */
 goog.ui.SplitPane = function(firstComponent, secondComponent, orientation,
     opt_domHelper) {
-  goog.ui.Component.call(this, opt_domHelper);
+  goog.base(this, opt_domHelper);
 
   /**
    * The orientation of the containers.
@@ -97,6 +97,12 @@
  * @enum {string}
  */
 goog.ui.SplitPane.EventType = {
+
+  /**
+   * Dispatched after handle drag.
+   */
+  HANDLE_DRAG: 'handle_drag',
+
   /**
    * Dispatched after handle drag end.
    */
@@ -296,8 +302,7 @@
  */
 goog.ui.SplitPane.prototype.canDecorate = function(element) {
   var className = goog.ui.SplitPane.FIRST_CONTAINER_CLASS_NAME_;
-  var firstContainer = goog.dom.getElementsByTagNameAndClass(
-      null, className, element)[0];
+  var firstContainer = this.getElementToDecorate_(element, className);
   if (!firstContainer) {
     return false;
   }
@@ -306,16 +311,15 @@
   this.firstComponentContainer_ = firstContainer;
 
   className = goog.ui.SplitPane.SECOND_CONTAINER_CLASS_NAME_;
-  var secondContainer = goog.dom.getElementsByTagNameAndClass(
-      null, className, element)[0];
+  var secondContainer = this.getElementToDecorate_(element, className);
+
   if (!secondContainer) {
     return false;
   }
   this.secondComponentContainer_ = secondContainer;
 
   className = goog.ui.SplitPane.HANDLE_CLASS_NAME_;
-  var splitpaneHandle = goog.dom.getElementsByTagNameAndClass(
-      null, className, element)[0];
+  var splitpaneHandle = this.getElementToDecorate_(element, className);
   if (!splitpaneHandle) {
     return false;
   }
@@ -327,6 +331,34 @@
 
 
 /**
+ * Obtains the element to be decorated by class name. If multiple such elements
+ * are found, preference is given to those directly attached to the specified
+ * root element.
+ * @param {Element} rootElement The root element from which to retrieve the
+ *     element to be decorated.
+ * @param {!string} className The target class name.
+ * @return {Element} The element to decorate.
+ * @private
+ */
+goog.ui.SplitPane.prototype.getElementToDecorate_ = function(rootElement,
+    className) {
+
+  // Decorate the root element's children, if available.
+  var childElements = goog.dom.getChildren(rootElement);
+  for (var i = 0; i < childElements.length; i++) {
+    var childElement = childElements[i];
+    if (goog.dom.classes.has(childElement, className)) {
+      return childElement;
+    }
+  }
+
+  // Default to the first descendent element with the correct class.
+  return goog.dom.getElementsByTagNameAndClass(
+      null, className, rootElement)[0];
+};
+
+
+/**
  * Decorates the given HTML element as a SplitPane.  Overrides {@link
  * goog.ui.Component#decorateInternal}.  Considered protected.
  * @param {Element} element Element (SplitPane div) to decorate.
@@ -334,7 +366,7 @@
  * @override
  */
 goog.ui.SplitPane.prototype.decorateInternal = function(element) {
-  goog.ui.SplitPane.superClass_.decorateInternal.call(this, element);
+  goog.base(this, 'decorateInternal', element);
 
   this.setUpHandle_();
 
@@ -385,7 +417,7 @@
  * @override
  */
 goog.ui.SplitPane.prototype.enterDocument = function() {
-  goog.ui.SplitPane.superClass_.enterDocument.call(this);
+  goog.base(this, 'enterDocument');
 
   // If position is not set in the inline style of the element, it is not
   // possible to get the element's real CSS position until the element is in
@@ -807,6 +839,7 @@
       var left = this.getRelativeLeft_(e.left);
       this.setFirstComponentSize(left);
     }
+    this.dispatchEvent(goog.ui.SplitPane.EventType.HANDLE_DRAG);
   }
 };
 
@@ -849,7 +882,7 @@
 
 /** @override */
 goog.ui.SplitPane.prototype.disposeInternal = function() {
-  goog.ui.SplitPane.superClass_.disposeInternal.call(this);
+  goog.base(this, 'disposeInternal');
 
   this.splitDragger_.dispose();
   this.splitDragger_ = null;
diff --git a/third_party/closure/goog/ui/textarea.js b/third_party/closure/goog/ui/textarea.js
index 2371c5a..3554751 100644
--- a/third_party/closure/goog/ui/textarea.js
+++ b/third_party/closure/goog/ui/textarea.js
@@ -42,7 +42,7 @@
  * @param {string} content Text to set as the textarea's value.
  * @param {goog.ui.TextareaRenderer=} opt_renderer Renderer used to render or
  *     decorate the textarea. Defaults to {@link goog.ui.TextareaRenderer}.
- * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM hepler, used for
+ * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
  *     document interaction.
  * @constructor
  * @extends {goog.ui.Control}
@@ -505,7 +505,7 @@
 
 
 /**
- * Resizes the texarea to shrink to fit its contents. The way this works is
+ * Resizes the textarea to shrink to fit its contents. The way this works is
  * by increasing the padding of the textarea by 1px (it's important here that
  * we're in box-sizing: border-box mode). If the size of the textarea grows,
  * then the box is filled up to the padding box with text.
@@ -516,12 +516,6 @@
   var textarea = this.getElement();
   if (!this.isResizing_) {
     this.isResizing_ = true;
-    var isEmpty = false;
-    if (!textarea.value) {
-      // Prevents height from becoming 0.
-      textarea.value = ' ';
-      isEmpty = true;
-    }
     var scrollHeight = textarea.scrollHeight;
     if (!scrollHeight) {
       this.setHeightToEstimate_();
@@ -550,9 +544,6 @@
         textarea.style.paddingBottom = paddingBox.bottom + 'px';
       }
     }
-    if (isEmpty) {
-      textarea.value = '';
-    }
     this.isResizing_ = false;
   }
 };
diff --git a/third_party/closure/goog/uri/uri.js b/third_party/closure/goog/uri/uri.js
index f80b3a6..c486935 100644
--- a/third_party/closure/goog/uri/uri.js
+++ b/third_party/closure/goog/uri/uri.js
@@ -1434,6 +1434,7 @@
   rv.encodedQuery_ = this.encodedQuery_;
   if (this.keyMap_) {
     rv.keyMap_ = this.keyMap_.clone();
+    rv.count_ = this.count_;
   }
   return rv;
 };
diff --git a/third_party/closure/goog/useragent/useragent.js b/third_party/closure/goog/useragent/useragent.js
index d77ac5c..c47c00f 100644
--- a/third_party/closure/goog/useragent/useragent.js
+++ b/third_party/closure/goog/useragent/useragent.js
@@ -474,16 +474,6 @@
 
 
 /**
- * Cache for {@link goog.userAgent.isDocumentMode}.
- * Browsers document mode version number is unlikely to change during a session
- * we cache the results.
- * @type {Object}
- * @private
- */
-goog.userAgent.isDocumentModeCache_ = {};
-
-
-/**
  * Whether the IE effective document mode is higher or the same as the given
  * document mode version.
  * NOTE: Only for IE, return false for another browser.
@@ -493,7 +483,24 @@
  *     same as the given version.
  */
 goog.userAgent.isDocumentMode = function(documentMode) {
-  return goog.userAgent.isDocumentModeCache_[documentMode] ||
-      (goog.userAgent.isDocumentModeCache_[documentMode] = goog.userAgent.IE &&
-      !!document.documentMode && document.documentMode >= documentMode);
+  return goog.userAgent.IE && goog.userAgent.DOCUMENT_MODE >= documentMode;
 };
+
+
+/**
+ * For IE version < 7, documentMode is undefined, so attempt to use the
+ * CSS1Compat property to see if we are in standards mode. If we are in
+ * standards mode, treat the browser version as the document mode. Otherwise,
+ * IE is emulating version 5.
+ * @type {number|undefined}
+ * @const
+ */
+goog.userAgent.DOCUMENT_MODE = (function() {
+  var doc = goog.global['document'];
+  if (!doc || !goog.userAgent.IE) {
+    return undefined;
+  }
+  var mode = goog.userAgent.getDocumentMode_();
+  return mode || (doc['compatMode'] == 'CSS1Compat' ?
+      parseInt(goog.userAgent.VERSION, 10) : 5);
+})();
diff --git a/third_party/closure/goog/vec/float64array.js b/third_party/closure/goog/vec/float64array.js
index 5d7908e..d58336f 100644
--- a/third_party/closure/goog/vec/float64array.js
+++ b/third_party/closure/goog/vec/float64array.js
@@ -98,8 +98,15 @@
  * goog.vec.Float64Array as Float64Array.
  */
 if (typeof Float64Array == 'undefined') {
-  goog.exportProperty(goog.vec.Float64Array, 'BYTES_PER_ELEMENT',
-                      goog.vec.Float64Array.BYTES_PER_ELEMENT);
+  try {
+    goog.exportProperty(goog.vec.Float64Array, 'BYTES_PER_ELEMENT',
+                        goog.vec.Float64Array.BYTES_PER_ELEMENT);
+  } catch (float64ArrayError) {
+    // Do nothing.  This code is in place to fix b/7225850, in which an error
+    // is incorrectly thrown for Google TV on an old Chrome.
+    // TODO(user): remove after that version is retired.
+  }
+
   goog.exportProperty(goog.vec.Float64Array.prototype, 'BYTES_PER_ELEMENT',
                       goog.vec.Float64Array.prototype.BYTES_PER_ELEMENT);
   goog.exportProperty(goog.vec.Float64Array.prototype, 'set',