diff --git a/javascript/node/selenium-webdriver/CHANGES.md b/javascript/node/selenium-webdriver/CHANGES.md
index 850719a09496e..594b8295218b3 100644
--- a/javascript/node/selenium-webdriver/CHANGES.md
+++ b/javascript/node/selenium-webdriver/CHANGES.md
@@ -16,6 +16,12 @@ the selenium-webdriver package README.
* Changed some `io` operations to use native promises.
* Changed `webdriver.CommandExecutor#execute()` and `HttpClient` to return
promises instead of using callback passing.
+* Changed the `webdriver.Serializable` class to an interface defined in the
+ `lib/serializable` module.
+* Changed the `Capabilities` class to extend the native `Map` type; Capabilities
+ implements Serializable to preseve existing functionality.
+* Changed the `Capabilities.has(key)` to only test if a capability has been set
+ (Map semantics); to check whether the value is true, use `get(key)`.
* Migrated the `webdriver.Command*` types from using the Closure Library to the
new `lib/command` module.
* Deprecated `executors.DeferredExecutor` in favor of
diff --git a/javascript/node/selenium-webdriver/lib/capabilities.js b/javascript/node/selenium-webdriver/lib/capabilities.js
new file mode 100644
index 0000000000000..b40354ed97f48
--- /dev/null
+++ b/javascript/node/selenium-webdriver/lib/capabilities.js
@@ -0,0 +1,395 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you 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.
+
+'use strict';
+
+/**
+ * @fileoverview Defines types related to describing the capabilities of a
+ * WebDriver session.
+ */
+
+const serializable = require('./serializable');
+
+
+/**
+ * Recognized browser names.
+ * @enum {string}
+ */
+const Browser = {
+ ANDROID: 'android',
+ CHROME: 'chrome',
+ FIREFOX: 'firefox',
+ IE: 'internet explorer',
+ INTERNET_EXPLORER: 'internet explorer',
+ IPAD: 'iPad',
+ IPHONE: 'iPhone',
+ OPERA: 'opera',
+ PHANTOM_JS: 'phantomjs',
+ SAFARI: 'safari',
+ HTMLUNIT: 'htmlunit'
+};
+
+
+/**
+ * Common Capability keys.
+ * @enum {string}
+ */
+const Capability = {
+
+ /**
+ * Indicates whether a driver should accept all SSL certs by default. This
+ * capability only applies when requesting a new session. To query whether
+ * a driver can handle insecure SSL certs, see {@link #SECURE_SSL}.
+ */
+ ACCEPT_SSL_CERTS: 'acceptSslCerts',
+
+
+ /**
+ * The browser name. Common browser names are defined in the {@link Browser}
+ * enum.
+ */
+ BROWSER_NAME: 'browserName',
+
+ /**
+ * Defines how elements should be scrolled into the viewport for interaction.
+ * This capability will be set to zero (0) if elements are aligned with the
+ * top of the viewport, or one (1) if aligned with the bottom. The default
+ * behavior is to align with the top of the viewport.
+ */
+ ELEMENT_SCROLL_BEHAVIOR: 'elementScrollBehavior',
+
+ /**
+ * Whether the driver is capable of handling modal alerts (e.g. alert,
+ * confirm, prompt). To define how a driver should handle alerts,
+ * use {@link #UNEXPECTED_ALERT_BEHAVIOR}.
+ */
+ HANDLES_ALERTS: 'handlesAlerts',
+
+ /**
+ * Key for the logging driver logging preferences.
+ */
+ LOGGING_PREFS: 'loggingPrefs',
+
+ /**
+ * Whether this session generates native events when simulating user input.
+ */
+ NATIVE_EVENTS: 'nativeEvents',
+
+ /**
+ * Describes the platform the browser is running on. Will be one of
+ * ANDROID, IOS, LINUX, MAC, UNIX, or WINDOWS. When requesting a
+ * session, ANY may be used to indicate no platform preference (this is
+ * semantically equivalent to omitting the platform capability).
+ */
+ PLATFORM: 'platform',
+
+ /**
+ * Describes the proxy configuration to use for a new WebDriver session.
+ */
+ PROXY: 'proxy',
+
+ /** Whether the driver supports changing the brower's orientation. */
+ ROTATABLE: 'rotatable',
+
+ /**
+ * Whether a driver is only capable of handling secure SSL certs. To request
+ * that a driver accept insecure SSL certs by default, use
+ * {@link #ACCEPT_SSL_CERTS}.
+ */
+ SECURE_SSL: 'secureSsl',
+
+ /** Whether the driver supports manipulating the app cache. */
+ SUPPORTS_APPLICATION_CACHE: 'applicationCacheEnabled',
+
+ /** Whether the driver supports locating elements with CSS selectors. */
+ SUPPORTS_CSS_SELECTORS: 'cssSelectorsEnabled',
+
+ /** Whether the browser supports JavaScript. */
+ SUPPORTS_JAVASCRIPT: 'javascriptEnabled',
+
+ /** Whether the driver supports controlling the browser's location info. */
+ SUPPORTS_LOCATION_CONTEXT: 'locationContextEnabled',
+
+ /** Whether the driver supports taking screenshots. */
+ TAKES_SCREENSHOT: 'takesScreenshot',
+
+ /**
+ * Defines how the driver should handle unexpected alerts. The value should
+ * be one of "accept", "dismiss", or "ignore.
+ */
+ UNEXPECTED_ALERT_BEHAVIOR: 'unexpectedAlertBehavior',
+
+ /** Defines the browser version. */
+ VERSION: 'version'
+};
+
+
+/**
+ * Describes how a proxy should be configured for a WebDriver session.
+ * Proxy configuration object, as defined by the WebDriver wire protocol.
+ * @typedef {(
+ * {proxyType: string}|
+ * {proxyType: string,
+ * proxyAutoconfigUrl: string}|
+ * {proxyType: string,
+ * ftpProxy: string,
+ * httpProxy: string,
+ * sslProxy: string,
+ * noProxy: string})}
+ */
+var ProxyConfig;
+
+
+/**
+ * Converts a generic hash object to a map.
+ * @param {!Object} hash The hash object.
+ * @return {!Map} The converted map.
+ */
+function toMap(hash) {
+ let m = new Map;
+ for (let key in hash) {
+ if (hash.hasOwnProperty(key)) {
+ m.set(key, hash[key]);
+ }
+ }
+ return m;
+}
+
+
+/**
+ * Describes a set of capabilities for a WebDriver session.
+ *
+ * @implements {serializable.Serializable>}
+ */
+class Capabilities extends Map {
+ /**
+ * @param {(Capabilities|Map|Object)=} opt_other Another set of
+ * capabilities to initialize this instance from.
+ */
+ constructor(opt_other) {
+ if (opt_other && !(opt_other instanceof Map)) {
+ opt_other = toMap(opt_other);
+ }
+ super(opt_other);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Android.
+ */
+ static android() {
+ return new Capabilities()
+ .set(Capability.BROWSER_NAME, Browser.ANDROID)
+ .set(Capability.PLATFORM, 'ANDROID');
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Chrome.
+ */
+ static chrome() {
+ return new Capabilities().set(Capability.BROWSER_NAME, Browser.CHROME);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Firefox.
+ */
+ static firefox() {
+ return new Capabilities().set(Capability.BROWSER_NAME, Browser.FIREFOX);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Internet Explorer.
+ */
+ static ie() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.INTERNET_EXPLORER).
+ set(Capability.PLATFORM, 'WINDOWS');
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for iPad.
+ */
+ static ipad() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.IPAD).
+ set(Capability.PLATFORM, 'MAC');
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for iPhone.
+ */
+ static iphone() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.IPHONE).
+ set(Capability.PLATFORM, 'MAC');
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Opera.
+ */
+ static opera() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.OPERA);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for PhantomJS.
+ */
+ static phantomjs() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.PHANTOM_JS);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for Safari.
+ */
+ static safari() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.SAFARI).
+ set(Capability.PLATFORM, 'MAC');
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for HTMLUnit.
+ */
+ static htmlunit() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.HTMLUNIT);
+ }
+
+ /**
+ * @return {!Capabilities} A basic set of capabilities for HTMLUnit
+ * with enabled Javascript.
+ */
+ static htmlunitwithjs() {
+ return new Capabilities().
+ set(Capability.BROWSER_NAME, Browser.HTMLUNIT).
+ set(Capability.SUPPORTS_JAVASCRIPT, true);
+ }
+
+ /**
+ * @return {!Object} The JSON representation of this instance.
+ * Note, the returned object may contain nested promised values.
+ * @override
+ */
+ serialize() {
+ let ret = {};
+ for (let key of this.keys()) {
+ ret[key] = this.get(key);
+ }
+ return ret;
+ }
+
+ /**
+ * Merges another set of capabilities into this instance.
+ * @param {!(Capabilities|Map|Object)} other The other
+ * set of capabilities to merge.
+ * @return {!Capabilities} A self reference.
+ */
+ merge(other) {
+ if (!other) {
+ throw new TypeError('no capabilities provided for merge');
+ }
+
+ if (!(other instanceof Map)) {
+ other = toMap(other);
+ }
+
+ for (let key of other.keys()) {
+ this.set(key, other.get(key));
+ }
+
+ return this;
+ }
+
+ /**
+ * @param {string} key The capability key.
+ * @param {*} value The capability value.
+ * @return {!Capabilities} A self reference.
+ * @throws {TypeError} If the `key` is not a string.
+ * @override
+ */
+ set(key, value) {
+ if (typeof key !== 'string') {
+ throw new TypeError('Capability keys must be strings: ' + typeof key);
+ }
+ super.set(key, value);
+ return this;
+ }
+
+ /**
+ * Sets the logging preferences. Preferences may be specified as a
+ * {@link ./logging.Preferences} instance, or a as a map of log-type to
+ * log-level.
+ * @param {!(./logging.Preferences|Object)} prefs The logging
+ * preferences.
+ * @return {!Capabilities} A self reference.
+ */
+ setLoggingPrefs(prefs) {
+ return this.set(Capability.LOGGING_PREFS, prefs);
+ }
+
+ /**
+ * Sets the proxy configuration for this instance.
+ * @param {ProxyConfig} proxy The desired proxy configuration.
+ * @return {!Capabilities} A self reference.
+ */
+ setProxy(proxy) {
+ return this.set(Capability.PROXY, proxy);
+ }
+
+ /**
+ * Sets whether native events should be used.
+ * @param {boolean} enabled Whether to enable native events.
+ * @return {!Capabilities} A self reference.
+ */
+ setEnableNativeEvents(enabled) {
+ return this.set(Capability.NATIVE_EVENTS, enabled);
+ }
+
+ /**
+ * Sets how elements should be scrolled into view for interaction.
+ * @param {number} behavior The desired scroll behavior: either 0 to align
+ * with the top of the viewport or 1 to align with the bottom.
+ * @return {!Capabilities} A self reference.
+ */
+ setScrollBehavior(behavior) {
+ return this.set(Capability.ELEMENT_SCROLL_BEHAVIOR, behavior);
+ }
+
+ /**
+ * Sets the default action to take with an unexpected alert before returning
+ * an error.
+ * @param {string} behavior The desired behavior; should be "accept",
+ * "dismiss", or "ignore". Defaults to "dismiss".
+ * @return {!Capabilities} A self reference.
+ */
+ setAlertBehavior(behavior) {
+ return this.set(Capability.UNEXPECTED_ALERT_BEHAVIOR, behavior);
+ }
+}
+serializable.setSerializable(Capabilities);
+
+
+// PUBLIC API
+
+
+exports.Browser = Browser;
+exports.Capabilities = Capabilities;
+exports.Capability = Capability;
+
+/** @typedef {ProxyConfig} */
+exports.ProxyConfig = ProxyConfig;
diff --git a/javascript/node/selenium-webdriver/lib/serializable.js b/javascript/node/selenium-webdriver/lib/serializable.js
new file mode 100644
index 0000000000000..a76b8dea51605
--- /dev/null
+++ b/javascript/node/selenium-webdriver/lib/serializable.js
@@ -0,0 +1,87 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you 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.
+
+'use strict';
+
+
+const SERIALIZABLE = Symbol('serializable');
+
+
+/**
+ * Defines an object that can be asynchronously serialized to its WebDriver
+ * wire representation.
+ *
+ * @template T
+ * @interface
+ */
+class Serializable {
+
+ /**
+ * Returns either this instance's serialized represention, if immediately
+ * available, or a promise for its serialized representation. This function is
+ * conceptually equivalent to objects that have a {@code toJSON()} property,
+ * except the serialize() result may be a promise or an object containing a
+ * promise (which are not directly JSON friendly).
+ *
+ * @return {!(T|IThenable)} This instance's serialized wire format.
+ */
+ serialize() {}
+}
+
+
+/**
+ * Marks a constructor as implementing the serializable interface.
+ *
+ * @param {function(new: ?)} ctor The constructor to update.
+ * @throws {TypeError} If the given value is not a constructor, or the
+ * constructor's prototype does not define a `serialize()` method.
+ */
+function setSerializable(ctor) {
+ if (!ctor || typeof ctor !== 'function') {
+ throw new TypeError('Input is not a constructor!');
+ }
+ if (typeof ctor.prototype.serialize !== 'function') {
+ throw new TypeError('Class does not define a "serialize" function');
+ }
+ ctor.prototype[SERIALIZABLE] = true;
+}
+
+
+ /**
+ * Checks if an object is marked as implementing the {@linkplain Serializable}
+ * interface.
+ *
+ * @param {*} obj The object to test.
+ * @return {boolean} Whether the given object implements the serializable
+ * interface.
+ * @see setSerializable
+ */
+function isSerializable(obj) {
+ try {
+ return obj && !!obj[SERIALIZABLE];
+ } catch (ignored) {
+ return false;
+ }
+}
+
+
+// PUBLIC API
+
+
+exports.Serializable = Serializable;
+exports.setSerializable = setSerializable;
+exports.isSerializable = isSerializable;
diff --git a/javascript/node/selenium-webdriver/test/lib/capabilities_test.js b/javascript/node/selenium-webdriver/test/lib/capabilities_test.js
new file mode 100644
index 0000000000000..84efc7895d379
--- /dev/null
+++ b/javascript/node/selenium-webdriver/test/lib/capabilities_test.js
@@ -0,0 +1,83 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you 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.
+
+'use strict';
+
+const Capabilities = require('../../lib/capabilities').Capabilities;
+
+const assert = require('assert');
+
+describe('Capabilities', function() {
+ it('can set and unset a capability', function() {
+ let caps = new Capabilities();
+ assert.equal(undefined, caps.get('foo'));
+ assert.ok(!caps.has('foo'));
+
+ caps.set('foo', 'bar');
+ assert.equal('bar', caps.get('foo'));
+ assert.ok(caps.has('foo'));
+
+ caps.set('foo', null);
+ assert.equal(null, caps.get('foo'));
+ assert.ok(caps.has('foo'));
+ });
+
+ it('requires string capability keys', function() {
+ let caps = new Capabilities();
+ assert.throws(() => caps.set({}, 'hi'));
+ });
+
+ it('can merge capabilities', function() {
+ let caps1 = new Capabilities()
+ .set('foo', 'bar')
+ .set('color', 'red');
+
+ let caps2 = new Capabilities()
+ .set('color', 'green');
+
+ assert.equal('bar', caps1.get('foo'));
+ assert.equal('red', caps1.get('color'));
+ assert.equal('green', caps2.get('color'));
+ assert.equal(undefined, caps2.get('foo'));
+
+ caps2.merge(caps1);
+ assert.equal('bar', caps1.get('foo'));
+ assert.equal('red', caps1.get('color'));
+ assert.equal('red', caps2.get('color'));
+ assert.equal('bar', caps2.get('foo'));
+ });
+
+ it('can be initialized from a hash object', function() {
+ let caps = new Capabilities({'one': 123, 'abc': 'def'});
+ assert.equal(123, caps.get('one'));
+ assert.equal('def', caps.get('abc'));
+ });
+
+ it('can be initialized from a map', function() {
+ let m = new Map([['one', 123], ['abc', 'def']]);
+
+ let caps = new Capabilities(m);
+ assert.equal(123, caps.get('one'));
+ assert.equal('def', caps.get('abc'));
+ });
+
+ it('can be serialized', function() {
+ let m = new Map([['one', 123], ['abc', 'def']]);
+ let caps = new Capabilities(m);
+ assert.deepEqual({one: 123, abc: 'def'}, caps.serialize());
+ });
+});