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()); + }); +});