diff --git a/javascript/node/selenium-webdriver/chrome.js b/javascript/node/selenium-webdriver/chrome.js index 1d26880c857cf..0d1eadb696c5d 100644 --- a/javascript/node/selenium-webdriver/chrome.js +++ b/javascript/node/selenium-webdriver/chrome.js @@ -127,20 +127,11 @@ 'use strict'; -const fs = require('fs'); -const util = require('util'); - const http = require('./http'); const io = require('./io'); -const {Browser, Capabilities, Capability} = require('./lib/capabilities'); -const command = require('./lib/command'); -const error = require('./lib/error'); -const logging = require('./lib/logging'); -const promise = require('./lib/promise'); -const Symbols = require('./lib/symbols'); -const webdriver = require('./lib/webdriver'); -const portprober = require('./net/portprober'); +const {Browser, Capabilities} = require('./lib/capabilities'); const remote = require('./remote'); +const chromium = require('./chromium'); /** @@ -152,81 +143,6 @@ const CHROMEDRIVER_EXE = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver'; -/** - * Custom command names supported by ChromeDriver. - * @enum {string} - */ -const Command = { - LAUNCH_APP: 'launchApp', - GET_NETWORK_CONDITIONS: 'getNetworkConditions', - SET_NETWORK_CONDITIONS: 'setNetworkConditions', - SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand', - GET_CAST_SINKS: 'getCastSinks', - SET_CAST_SINK_TO_USE: 'setCastSinkToUse', - START_CAST_TAB_MIRRORING: 'setCastTabMirroring', - GET_CAST_ISSUE_MESSAGE: 'getCastIssueMessage', - STOP_CASTING: 'stopCasting', -}; - - -/** - * Creates a command executor with support for ChromeDriver's custom commands. - * @param {!Promise} url The server's URL. - * @return {!command.Executor} The new command executor. - */ -function createExecutor(url) { - let agent = new http.Agent({ keepAlive: true }); - let client = url.then(url => new http.HttpClient(url, agent)); - let executor = new http.Executor(client); - configureExecutor(executor); - return executor; -} - - -/** - * Configures the given executor with Chrome-specific commands. - * @param {!http.Executor} executor the executor to configure. - */ -function configureExecutor(executor) { - executor.defineCommand( - Command.LAUNCH_APP, - 'POST', - '/session/:sessionId/chromium/launch_app'); - executor.defineCommand( - Command.GET_NETWORK_CONDITIONS, - 'GET', - '/session/:sessionId/chromium/network_conditions'); - executor.defineCommand( - Command.SET_NETWORK_CONDITIONS, - 'POST', - '/session/:sessionId/chromium/network_conditions'); - executor.defineCommand( - Command.SEND_DEVTOOLS_COMMAND, - 'POST', - '/session/:sessionId/chromium/send_command'); - executor.defineCommand( - Command.GET_CAST_SINKS, - 'GET', - '/session/:sessionId/goog/cast/get_sinks'); - executor.defineCommand( - Command.SET_CAST_SINK_TO_USE, - 'POST', - '/session/:sessionId/goog/cast/set_sink_to_use'); - executor.defineCommand( - Command.START_CAST_TAB_MIRRORING, - 'POST', - '/session/:sessionId/goog/cast/start_tab_mirroring'); - executor.defineCommand( - Command.GET_CAST_ISSUE_MESSAGE, - 'GET', - '/session/:sessionId/goog/cast/get_issue_message'); - executor.defineCommand( - Command.STOP_CASTING, - 'POST', - '/session/:sessionId/goog/cast/stop_casting'); -} - - /** * _Synchronously_ attempts to locate the chromedriver executable on the current * system. @@ -243,7 +159,7 @@ function locateSynchronously() { * a [ChromeDriver](https://chromedriver.chromium.org/) * server in a child process. */ -class ServiceBuilder extends remote.DriverService.Builder { +class ServiceBuilder extends chromium.ServiceBuilder { /** * @param {string=} opt_exe Path to the server executable to use. If omitted, * the builder will attempt to locate the chromedriver on the current @@ -262,60 +178,10 @@ class ServiceBuilder extends remote.DriverService.Builder { } super(exe); - this.setLoopback(true); // Required - } - - /** - * Sets which port adb is listening to. _The ChromeDriver will connect to adb - * if an {@linkplain Options#androidPackage Android session} is requested, but - * adb **must** be started beforehand._ - * - * @param {number} port Which port adb is running on. - * @return {!ServiceBuilder} A self reference. - */ - setAdbPort(port) { - return this.addArguments('--adb-port=' + port); - } - - /** - * Sets the path of the log file the driver should log to. If a log file is - * not specified, the driver will log to stderr. - * @param {string} path Path of the log file to use. - * @return {!ServiceBuilder} A self reference. - */ - loggingTo(path) { - return this.addArguments('--log-path=' + path); - } - - /** - * Enables verbose logging. - * @return {!ServiceBuilder} A self reference. - */ - enableVerboseLogging() { - return this.addArguments('--verbose'); - } - - /** - * Sets the number of threads the driver should use to manage HTTP requests. - * By default, the driver will use 4 threads. - * @param {number} n The number of threads to use. - * @return {!ServiceBuilder} A self reference. - */ - setNumHttpThreads(n) { - return this.addArguments('--http-threads=' + n); - } - - /** - * @override - */ - setPath(path) { - super.setPath(path); - return this.addArguments('--url-base=' + path); } } - /** @type {remote.DriverService} */ let defaultService = null; @@ -349,111 +215,10 @@ function getDefaultService() { } -const OPTIONS_CAPABILITY_KEY = 'goog:chromeOptions'; - - /** * Class for managing ChromeDriver specific options. */ -class Options extends Capabilities { - /** - * @param {(Capabilities|Map|Object)=} other Another set of - * capabilities to initialize this instance from. - */ - constructor(other = undefined) { - super(other); - - /** @private {!Object} */ - this.options_ = this.get(OPTIONS_CAPABILITY_KEY) || {}; - - this.setBrowserName(Browser.CHROME); - this.set(OPTIONS_CAPABILITY_KEY, this.options_); - } - - /** - * Add additional command line arguments to use when launching the Chrome - * browser. Each argument may be specified with or without the "--" prefix - * (e.g. "--foo" and "foo"). Arguments with an associated value should be - * delimited by an "=": "foo=bar". - * - * @param {...(string|!Array)} args The arguments to add. - * @return {!Options} A self reference. - */ - addArguments(...args) { - let newArgs = (this.options_.args || []).concat(...args); - if (newArgs.length) { - this.options_.args = newArgs; - } - return this; - } - - /** - * Configures the chromedriver to start Chrome in headless mode. - * - * > __NOTE:__ Resizing the browser window in headless mode is only supported - * > in Chrome 60. Users are encouraged to set an initial window size with - * > the {@link #windowSize windowSize({width, height})} option. - * - * > __NOTE__: For security, Chrome disables downloads by default when - * > in headless mode (to prevent sites from silently downloading files to - * > your machine). After creating a session, you may call - * > {@link ./chrome.Driver#setDownloadPath setDownloadPath} to re-enable - * > downloads, saving files in the specified directory. - * - * @return {!Options} A self reference. - */ - headless() { - return this.addArguments('headless'); - } - - /** - * Sets the initial window size. - * - * @param {{width: number, height: number}} size The desired window size. - * @return {!Options} A self reference. - * @throws {TypeError} if width or height is unspecified, not a number, or - * less than or equal to 0. - */ - windowSize({width, height}) { - function checkArg(arg) { - if (typeof arg !== 'number' || arg <= 0) { - throw TypeError('Arguments must be {width, height} with numbers > 0'); - } - } - checkArg(width); - checkArg(height); - return this.addArguments(`window-size=${width},${height}`); - } - - /** - * List of Chrome command line switches to exclude that ChromeDriver by default - * passes when starting Chrome. Do not prefix switches with "--". - * - * @param {...(string|!Array)} args The switches to exclude. - * @return {!Options} A self reference. - */ - excludeSwitches(...args) { - let switches = (this.options_.excludeSwitches || []).concat(...args); - if (switches.length) { - this.options_.excludeSwitches = switches; - } - return this; - } - - /** - * Add additional extensions to install when launching Chrome. Each extension - * should be specified as the path to the packed CRX file, or a Buffer for an - * extension. - * @param {...(string|!Buffer|!Array<(string|!Buffer)>)} args The - * extensions to add. - * @return {!Options} A self reference. - */ - addExtensions(...args) { - let current = this.options_.extensions || []; - this.options_.extensions = current.concat(...args); - return this; - } - +class Options extends chromium.Options { /** * Sets the path to the Chrome binary to use. On Mac OS X, this path should * reference the actual Chrome executable, not just the application binary @@ -466,98 +231,7 @@ class Options extends Capabilities { * @return {!Options} A self reference. */ setChromeBinaryPath(path) { - this.options_.binary = path; - return this; - } - - /** - * Sets whether to leave the started Chrome browser running if the controlling - * ChromeDriver service is killed before {@link webdriver.WebDriver#quit()} is - * called. - * @param {boolean} detach Whether to leave the browser running if the - * chromedriver service is killed before the session. - * @return {!Options} A self reference. - */ - detachDriver(detach) { - this.options_.detach = detach; - return this; - } - - /** - * Sets the user preferences for Chrome's user profile. See the "Preferences" - * file in Chrome's user data directory for examples. - * @param {!Object} prefs Dictionary of user preferences to use. - * @return {!Options} A self reference. - */ - setUserPreferences(prefs) { - this.options_.prefs = prefs; - return this; - } - - /** - * Sets the performance logging preferences. Options include: - * - * - `enableNetwork`: Whether or not to collect events from Network domain. - * - `enablePage`: Whether or not to collect events from Page domain. - * - `enableTimeline`: Whether or not to collect events from Timeline domain. - * Note: when tracing is enabled, Timeline domain is implicitly disabled, - * unless `enableTimeline` is explicitly set to true. - * - `tracingCategories`: A comma-separated string of Chrome tracing - * categories for which trace events should be collected. An unspecified - * or empty string disables tracing. - * - `bufferUsageReportingInterval`: The requested number of milliseconds - * between DevTools trace buffer usage events. For example, if 1000, then - * once per second, DevTools will report how full the trace buffer is. If - * a report indicates the buffer usage is 100%, a warning will be issued. - * - * @param {{enableNetwork: boolean, - * enablePage: boolean, - * enableTimeline: boolean, - * tracingCategories: string, - * bufferUsageReportingInterval: number}} prefs The performance - * logging preferences. - * @return {!Options} A self reference. - */ - setPerfLoggingPrefs(prefs) { - this.options_.perfLoggingPrefs = prefs; - return this; - } - - /** - * Sets preferences for the "Local State" file in Chrome's user data - * directory. - * @param {!Object} state Dictionary of local state preferences. - * @return {!Options} A self reference. - */ - setLocalState(state) { - this.options_.localState = state; - return this; - } - - /** - * Sets the name of the activity hosting a Chrome-based Android WebView. This - * option must be set to connect to an [Android WebView]( - * https://chromedriver.chromium.org/getting-started/getting-started---android) - * - * @param {string} name The activity name. - * @return {!Options} A self reference. - */ - androidActivity(name) { - this.options_.androidActivity = name; - return this; - } - - /** - * Sets the device serial number to connect to via ADB. If not specified, the - * ChromeDriver will select an unused device at random. An error will be - * returned if all devices already have active sessions. - * - * @param {string} serial The device serial number to connect to. - * @return {!Options} A self reference. - */ - androidDeviceSerial(serial) { - this.options_.androidDeviceSerial = serial; - return this; + return this.setBinaryPath(path); } /** @@ -570,44 +244,6 @@ class Options extends Capabilities { return this.androidPackage('com.android.chrome'); } - /** - * Sets the package name of the Chrome or WebView app. - * - * @param {?string} pkg The package to connect to, or `null` to disable Android - * and switch back to using desktop Chrome. - * @return {!Options} A self reference. - */ - androidPackage(pkg) { - this.options_.androidPackage = pkg; - return this; - } - - /** - * Sets the process name of the Activity hosting the WebView (as given by - * `ps`). If not specified, the process name is assumed to be the same as - * {@link #androidPackage}. - * - * @param {string} processName The main activity name. - * @return {!Options} A self reference. - */ - androidProcess(processName) { - this.options_.androidProcess = processName; - return this; - } - - /** - * Sets whether to connect to an already-running instead of the specified - * {@linkplain #androidProcess app} instead of launching the app with a clean - * data directory. - * - * @param {boolean} useRunning Whether to connect to a running instance. - * @return {!Options} A self reference. - */ - androidUseRunningApp(useRunning) { - this.options_.androidUseRunningApp = useRunning; - return this; - } - /** * Sets the path to Chrome's log file. This path should exist on the machine * that will launch Chrome. @@ -615,8 +251,7 @@ class Options extends Capabilities { * @return {!Options} A self reference. */ setChromeLogFile(path) { - this.options_.logPath = path; - return this; + return this.setBrowserLogFile(path); } /** @@ -626,79 +261,18 @@ class Options extends Capabilities { * @return {!Options} A self reference. */ setChromeMinidumpPath(path) { - this.options_.minidumpPath = path; - return this; - } - - /** - * Configures Chrome to emulate a mobile device. For more information, refer - * to the ChromeDriver project page on [mobile emulation][em]. Configuration - * options include: - * - * - `deviceName`: The name of a pre-configured [emulated device][devem] - * - `width`: screen width, in pixels - * - `height`: screen height, in pixels - * - `pixelRatio`: screen pixel ratio - * - * __Example 1: Using a Pre-configured Device__ - * - * let options = new chrome.Options().setMobileEmulation( - * {deviceName: 'Google Nexus 5'}); - * - * let driver = chrome.Driver.createSession(options); - * - * __Example 2: Using Custom Screen Configuration__ - * - * let options = new chrome.Options().setMobileEmulation({ - * width: 360, - * height: 640, - * pixelRatio: 3.0 - * }); - * - * let driver = chrome.Driver.createSession(options); - * - * - * [em]: https://chromedriver.chromium.org/mobile-emulation - * [devem]: https://developer.chrome.com/devtools/docs/device-mode - * - * @param {?({deviceName: string}| - * {width: number, height: number, pixelRatio: number})} config The - * mobile emulation configuration, or `null` to disable emulation. - * @return {!Options} A self reference. - */ - setMobileEmulation(config) { - this.options_.mobileEmulation = config; - return this; - } - - /** - * Converts this instance to its JSON wire protocol representation. Note this - * function is an implementation not intended for general use. - * - * @return {!Object} The JSON wire protocol representation of this instance. - * @suppress {checkTypes} Suppress [] access on a struct. - */ - [Symbols.serialize]() { - if (this.options_.extensions && this.options_.extensions.length) { - this.options_.extensions = - this.options_.extensions.map(function(extension) { - if (Buffer.isBuffer(extension)) { - return extension.toString('base64'); - } - return io.read(/** @type {string} */(extension)) - .then(buffer => buffer.toString('base64')); - }); - } - return super[Symbols.serialize](); + return this.setBrowserMinidumpPath(path); } } +Options.prototype.CAPABILITY_KEY = 'goog:chromeOptions'; +Options.prototype.BROWSER_NAME_VALUE = Browser.CHROME; + /** * Creates a new WebDriver client for Chrome. */ -class Driver extends webdriver.WebDriver { - +class Driver extends chromium.Driver { /** * Creates a new session with the ChromeDriver. * @@ -711,188 +285,15 @@ class Driver extends webdriver.WebDriver { * @return {!Driver} A new driver instance. */ static createSession(opt_config, opt_serviceExecutor) { - let executor; - let onQuit; - if (opt_serviceExecutor instanceof http.Executor) { - executor = opt_serviceExecutor; - configureExecutor(executor); - } else { - let service = opt_serviceExecutor || getDefaultService(); - executor = createExecutor(service.start()); - onQuit = () => service.kill(); - } - - let caps = opt_config || Capabilities.chrome(); - - // W3C spec requires noProxy value to be an array of strings, but Chrome - // expects a single host as a string. - let proxy = caps.get(Capability.PROXY); - if (proxy && Array.isArray(proxy.noProxy)) { - proxy.noProxy = proxy.noProxy[0]; - if (!proxy.noProxy) { - proxy.noProxy = undefined; - } - } - - return /** @type {!Driver} */(super.createSession(executor, caps, onQuit)); - } - - /** - * This function is a no-op as file detectors are not supported by this - * implementation. - * @override - */ - setFileDetector() {} - - /** - * Schedules a command to launch Chrome App with given ID. - * @param {string} id ID of the App to launch. - * @return {!Promise} A promise that will be resolved - * when app is launched. - */ - launchApp(id) { - return this.execute( - new command.Command(Command.LAUNCH_APP).setParameter('id', id)); - } - - /** - * Schedules a command to get Chrome network emulation settings. - * @return {!Promise} A promise that will be resolved when network - * emulation settings are retrievied. - */ - getNetworkConditions() { - return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS)); - } - - /** - * Schedules a command to set Chrome network emulation settings. - * - * __Sample Usage:__ - * - * driver.setNetworkConditions({ - * offline: false, - * latency: 5, // Additional latency (ms). - * download_throughput: 500 * 1024, // Maximal aggregated download throughput. - * upload_throughput: 500 * 1024 // Maximal aggregated upload throughput. - * }); - * - * @param {Object} spec Defines the network conditions to set - * @return {!Promise} A promise that will be resolved when network - * emulation settings are set. - */ - setNetworkConditions(spec) { - if (!spec || typeof spec !== 'object') { - throw TypeError('setNetworkConditions called with non-network-conditions parameter'); - } - return this.execute( - new command.Command(Command.SET_NETWORK_CONDITIONS) - .setParameter('network_conditions', spec)); - } - - /** - * Sends an arbitrary devtools command to the browser. - * - * @param {string} cmd The name of the command to send. - * @param {Object=} params The command parameters. - * @return {!Promise} A promise that will be resolved when the command - * has finished. - * @see - */ - sendDevToolsCommand(cmd, params = {}) { - return this.execute( - new command.Command(Command.SEND_DEVTOOLS_COMMAND) - .setParameter('cmd', cmd) - .setParameter('params', params)); - } - - /** - * Sends a DevTools command to change Chrome's download directory. - * - * @param {string} path The desired download directory. - * @return {!Promise} A promise that will be resolved when the command - * has finished. - * @see #sendDevToolsCommand - */ - async setDownloadPath(path) { - if (!path || typeof path !== 'string') { - throw new error.InvalidArgumentError('invalid download path'); - } - const stat = await io.stat(path); - if (!stat.isDirectory()) { - throw new error.InvalidArgumentError('not a directory: ' + path); - } - return this.sendDevToolsCommand('Page.setDownloadBehavior', { - 'behavior': 'allow', - 'downloadPath': path - }); - } - - - /** - * Returns the list of cast sinks (Cast devices) available to the Chrome media router. - * - * @return {!promise.Thenable} A promise that will be resolved with an array of Strings - * containing the friendly device names of available cast sink targets. - */ - getCastSinks() { - return this.schedule( - new command.Command(Command.GET_CAST_SINKS), - 'Driver.getCastSinks()'); + let caps = opt_config || new Options(); + return /** @type {!Driver} */(super.createSession(caps, opt_serviceExecutor)); } - /** - * Selects a cast sink (Cast device) as the recipient of media router intents (connect or play). - * - * @param {String} Friendly name of the target device. - * @return {!promise.Thenable} A promise that will be resolved - * when the target device has been selected to respond further webdriver commands. - */ - setCastSinkToUse(deviceName) { - return this.schedule( - new command.Command(Command.SET_CAST_SINK_TO_USE).setParameter('sinkName', deviceName), - 'Driver.setCastSinkToUse(' + deviceName + ')'); - } - - /** - * Initiates tab mirroring for the current browser tab on the specified device. - * - * @param {String} Friendly name of the target device. - * @return {!promise.Thenable} A promise that will be resolved - * when the mirror command has been issued to the device. - */ - startCastTabMirroring(deviceName) { - return this.schedule( - new command.Command(Command.START_CAST_TAB_MIRRORING).setParameter('sinkName', deviceName), - 'Driver.startCastTabMirroring(' + deviceName + ')'); - } - - /** - * a - * - * @param {String} Friendly name of the target device. - * @return {!promise.Thenable} A promise that will be resolved - * when the mirror command has been issued to the device. - */ - getCastIssueMessage() { - return this.schedule( - new command.Command(Command.GET_CAST_ISSUE_MESSAGE), - 'Driver.getCastIssueMessage()'); - } - - /** - * Stops casting from media router to the specified device, if connected. - * - * @param {String} Friendly name of the target device. - * @return {!promise.Thenable} A promise that will be resolved - * when the stop command has been issued to the device. - */ - stopCasting(deviceName) { - return this.schedule( - new command.Command(Command.STOP_CASTING).setParameter('sinkName', deviceName), - 'Driver.stopCasting(' + deviceName + ')'); - } + static getDefaultService = getDefaultService; } +Driver.prototype.VENDOR_COMMAND_PREFIX = "goog"; + // PUBLIC API diff --git a/javascript/node/selenium-webdriver/chromium.js b/javascript/node/selenium-webdriver/chromium.js new file mode 100644 index 0000000000000..d828ce5073d3d --- /dev/null +++ b/javascript/node/selenium-webdriver/chromium.js @@ -0,0 +1,762 @@ +// 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. + +/** + * @fileoverview Defines an abstract {@linkplain Driver WebDriver} client for + * Chromium-based web browsers. These classes should not be instantiated + * directly. + * + * There are three primary classes exported by this module: + * + * 1. {@linkplain ServiceBuilder}: configures the + * {@link selenium-webdriver/remote.DriverService remote.DriverService} + * that manages a WebDriver server child process. + * + * 2. {@linkplain Options}: defines configuration options for each new Chromium + * session, such as which {@linkplain Options#setProxy proxy} to use, + * what {@linkplain Options#addExtensions extensions} to install, or + * what {@linkplain Options#addArguments command-line switches} to use when + * starting the browser. + * + * 3. {@linkplain Driver}: the WebDriver client; each new instance will control + * a unique browser session with a clean user profile (unless otherwise + * configured through the {@link Options} class). + * + * __Headless Chromium__ + * + * To start the browser in headless mode, simply call + * {@linkplain Options#headless Options.headless()}. + * + * let chrome = require('selenium-webdriver/chrome'); + * let {Builder} = require('selenium-webdriver'); + * + * let driver = new Builder() + * .forBrowser('chrome') + * .setChromeOptions(new chrome.Options().headless()) + * .build(); + * + * __Customizing the Chromium WebDriver Server__ + * + * Subclasses of {@link Driver} are expected to provide a static + * getDefaultService method. By default, this method will be called every time + * a {@link Driver} instance is created to obtain the default driver service + * for that specific browser (e.g. Chrome or Chromium Edge). Subclasses are + * responsible for managing the lifetime of the default service. + * + * You may also create a {@link Driver} with its own driver service. This is + * useful if you need to capture the server's log output for a specific session: + * + * let chrome = require('selenium-webdriver/chrome'); + * + * let service = new chrome.ServiceBuilder() + * .loggingTo('/my/log/file.txt') + * .enableVerboseLogging() + * .build(); + * + * let options = new chrome.Options(); + * // configure browser options ... + * + * let driver = chrome.Driver.createSession(options, service); + */ + +'use strict'; + +const http = require('./http'); +const io = require('./io'); +const {Capabilities, Capability} = require('./lib/capabilities'); +const command = require('./lib/command'); +const error = require('./lib/error'); +const promise = require('./lib/promise'); +const Symbols = require('./lib/symbols'); +const webdriver = require('./lib/webdriver'); +const remote = require('./remote'); + + +/** + * Custom command names supported by Chromium WebDriver. + * @enum {string} + */ +const Command = { + LAUNCH_APP: 'launchApp', + GET_NETWORK_CONDITIONS: 'getNetworkConditions', + SET_NETWORK_CONDITIONS: 'setNetworkConditions', + SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand', + GET_CAST_SINKS: 'getCastSinks', + SET_CAST_SINK_TO_USE: 'setCastSinkToUse', + START_CAST_TAB_MIRRORING: 'setCastTabMirroring', + GET_CAST_ISSUE_MESSAGE: 'getCastIssueMessage', + STOP_CASTING: 'stopCasting', +}; + + +/** + * Creates a command executor with support for Chromium's custom commands. + * @param {!Promise} url The server's URL. + * @return {!command.Executor} The new command executor. + */ +function createExecutor(url, vendorPrefix) { + let agent = new http.Agent({ keepAlive: true }); + let client = url.then(url => new http.HttpClient(url, agent)); + let executor = new http.Executor(client); + configureExecutor(executor, vendorPrefix); + return executor; +} + + +/** + * Configures the given executor with Chromium-specific commands. + * @param {!http.Executor} executor the executor to configure. + */ +function configureExecutor(executor, vendorPrefix) { + executor.defineCommand( + Command.LAUNCH_APP, + 'POST', + '/session/:sessionId/chromium/launch_app'); + executor.defineCommand( + Command.GET_NETWORK_CONDITIONS, + 'GET', + '/session/:sessionId/chromium/network_conditions'); + executor.defineCommand( + Command.SET_NETWORK_CONDITIONS, + 'POST', + '/session/:sessionId/chromium/network_conditions'); + executor.defineCommand( + Command.SEND_DEVTOOLS_COMMAND, + 'POST', + '/session/:sessionId/chromium/send_command'); + executor.defineCommand( + Command.GET_CAST_SINKS, + 'GET', + `/session/:sessionId/${vendorPrefix}/cast/get_sinks`); + executor.defineCommand( + Command.SET_CAST_SINK_TO_USE, + 'POST', + `/session/:sessionId/${vendorPrefix}/cast/set_sink_to_use`); + executor.defineCommand( + Command.START_CAST_TAB_MIRRORING, + 'POST', + `/session/:sessionId/${vendorPrefix}/cast/start_tab_mirroring`); + executor.defineCommand( + Command.GET_CAST_ISSUE_MESSAGE, + 'GET', + `/session/:sessionId/${vendorPrefix}/cast/get_issue_message`); + executor.defineCommand( + Command.STOP_CASTING, + 'POST', + `/session/:sessionId/${vendorPrefix}/cast/stop_casting`); +} + + +/** + * Creates {@link selenium-webdriver/remote.DriverService} instances that manage + * a WebDriver server in a child process. + */ +class ServiceBuilder extends remote.DriverService.Builder { + /** + * @param {string=} exe Path to the server executable to use. Subclasses + * should ensure a valid path to the appropriate exe is provided. + */ + constructor(exe) { + super(exe); + this.setLoopback(true); // Required + } + + /** + * Sets which port adb is listening to. _The driver will connect to adb + * if an {@linkplain Options#androidPackage Android session} is requested, but + * adb **must** be started beforehand._ + * + * @param {number} port Which port adb is running on. + * @return {!ServiceBuilder} A self reference. + */ + setAdbPort(port) { + return this.addArguments('--adb-port=' + port); + } + + /** + * Sets the path of the log file the driver should log to. If a log file is + * not specified, the driver will log to stderr. + * @param {string} path Path of the log file to use. + * @return {!ServiceBuilder} A self reference. + */ + loggingTo(path) { + return this.addArguments('--log-path=' + path); + } + + /** + * Enables verbose logging. + * @return {!ServiceBuilder} A self reference. + */ + enableVerboseLogging() { + return this.addArguments('--verbose'); + } + + /** + * Sets the number of threads the driver should use to manage HTTP requests. + * By default, the driver will use 4 threads. + * @param {number} n The number of threads to use. + * @return {!ServiceBuilder} A self reference. + */ + setNumHttpThreads(n) { + return this.addArguments('--http-threads=' + n); + } + + /** + * @override + */ + setPath(path) { + super.setPath(path); + return this.addArguments('--url-base=' + path); + } +} + + +/** + * Class for managing WebDriver options specific to a Chromium-based browser. + */ +class Options extends Capabilities { + /** + * @param {(Capabilities|Map|Object)=} other Another set of + * capabilities to initialize this instance from. + */ + constructor(other = undefined) { + super(other); + + /** @private {!Object} */ + this.options_ = this.get(this.CAPABILITY_KEY) || {}; + + this.setBrowserName(this.BROWSER_NAME_VALUE); + this.set(this.CAPABILITY_KEY, this.options_); + } + + /** + * Add additional command line arguments to use when launching the browser. + * Each argument may be specified with or without the "--" prefix + * (e.g. "--foo" and "foo"). Arguments with an associated value should be + * delimited by an "=": "foo=bar". + * + * @param {...(string|!Array)} args The arguments to add. + * @return {!Options} A self reference. + */ + addArguments(...args) { + let newArgs = (this.options_.args || []).concat(...args); + if (newArgs.length) { + this.options_.args = newArgs; + } + return this; + } + + /** + * Configures the driver to start the browser in headless mode. + * + * > __NOTE:__ Resizing the browser window in headless mode is only supported + * > in Chromium 60+. Users are encouraged to set an initial window size with + * > the {@link #windowSize windowSize({width, height})} option. + * + * > __NOTE__: For security, Chromium disables downloads by default when + * > in headless mode (to prevent sites from silently downloading files to + * > your machine). After creating a session, you may call + * > {@link ./chrome.Driver#setDownloadPath setDownloadPath} to re-enable + * > downloads, saving files in the specified directory. + * + * @return {!Options} A self reference. + */ + headless() { + return this.addArguments('headless'); + } + + /** + * Sets the initial window size. + * + * @param {{width: number, height: number}} size The desired window size. + * @return {!Options} A self reference. + * @throws {TypeError} if width or height is unspecified, not a number, or + * less than or equal to 0. + */ + windowSize({width, height}) { + function checkArg(arg) { + if (typeof arg !== 'number' || arg <= 0) { + throw TypeError('Arguments must be {width, height} with numbers > 0'); + } + } + checkArg(width); + checkArg(height); + return this.addArguments(`window-size=${width},${height}`); + } + + /** + * List of Chrome command line switches to exclude that ChromeDriver by default + * passes when starting Chrome. Do not prefix switches with "--". + * + * @param {...(string|!Array)} args The switches to exclude. + * @return {!Options} A self reference. + */ + excludeSwitches(...args) { + let switches = (this.options_.excludeSwitches || []).concat(...args); + if (switches.length) { + this.options_.excludeSwitches = switches; + } + return this; + } + + /** + * Add additional extensions to install when launching the browser. Each extension + * should be specified as the path to the packed CRX file, or a Buffer for an + * extension. + * @param {...(string|!Buffer|!Array<(string|!Buffer)>)} args The + * extensions to add. + * @return {!Options} A self reference. + */ + addExtensions(...args) { + let current = this.options_.extensions || []; + this.options_.extensions = current.concat(...args); + return this; + } + + /** + * Sets the path to the browser binary to use. On Mac OS X, this path should + * reference the actual Chromium executable, not just the application binary + * (e.g. "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"). + * + * The binary path can be absolute or relative to the WebDriver server + * executable, but it must exist on the machine that will launch the browser. + * + * @param {string} path The path to the browser binary to use. + * @return {!Options} A self reference. + */ + setBinaryPath(path) { + this.options_.binary = path; + return this; + } + + /** + * Sets whether to leave the started browser process running if the controlling + * driver service is killed before {@link webdriver.WebDriver#quit()} is + * called. + * @param {boolean} detach Whether to leave the browser running if the + * driver service is killed before the session. + * @return {!Options} A self reference. + */ + detachDriver(detach) { + this.options_.detach = detach; + return this; + } + + /** + * Sets the user preferences for Chrome's user profile. See the "Preferences" + * file in Chrome's user data directory for examples. + * @param {!Object} prefs Dictionary of user preferences to use. + * @return {!Options} A self reference. + */ + setUserPreferences(prefs) { + this.options_.prefs = prefs; + return this; + } + + /** + * Sets the performance logging preferences. Options include: + * + * - `enableNetwork`: Whether or not to collect events from Network domain. + * - `enablePage`: Whether or not to collect events from Page domain. + * - `enableTimeline`: Whether or not to collect events from Timeline domain. + * Note: when tracing is enabled, Timeline domain is implicitly disabled, + * unless `enableTimeline` is explicitly set to true. + * - `tracingCategories`: A comma-separated string of Chromium tracing + * categories for which trace events should be collected. An unspecified + * or empty string disables tracing. + * - `bufferUsageReportingInterval`: The requested number of milliseconds + * between DevTools trace buffer usage events. For example, if 1000, then + * once per second, DevTools will report how full the trace buffer is. If + * a report indicates the buffer usage is 100%, a warning will be issued. + * + * @param {{enableNetwork: boolean, + * enablePage: boolean, + * enableTimeline: boolean, + * tracingCategories: string, + * bufferUsageReportingInterval: number}} prefs The performance + * logging preferences. + * @return {!Options} A self reference. + */ + setPerfLoggingPrefs(prefs) { + this.options_.perfLoggingPrefs = prefs; + return this; + } + + /** + * Sets preferences for the "Local State" file in Chrome's user data + * directory. + * @param {!Object} state Dictionary of local state preferences. + * @return {!Options} A self reference. + */ + setLocalState(state) { + this.options_.localState = state; + return this; + } + + /** + * Sets the name of the activity hosting a Chrome-based Android WebView. This + * option must be set to connect to an [Android WebView]( + * https://chromedriver.chromium.org/getting-started/getting-started---android) + * + * @param {string} name The activity name. + * @return {!Options} A self reference. + */ + androidActivity(name) { + this.options_.androidActivity = name; + return this; + } + + /** + * Sets the device serial number to connect to via ADB. If not specified, the + * WebDriver server will select an unused device at random. An error will be + * returned if all devices already have active sessions. + * + * @param {string} serial The device serial number to connect to. + * @return {!Options} A self reference. + */ + androidDeviceSerial(serial) { + this.options_.androidDeviceSerial = serial; + return this; + } + + /** + * Sets the package name of the Chrome or WebView app. + * + * @param {?string} pkg The package to connect to, or `null` to disable Android + * and switch back to using desktop browser. + * @return {!Options} A self reference. + */ + androidPackage(pkg) { + this.options_.androidPackage = pkg; + return this; + } + + /** + * Sets the process name of the Activity hosting the WebView (as given by + * `ps`). If not specified, the process name is assumed to be the same as + * {@link #androidPackage}. + * + * @param {string} processName The main activity name. + * @return {!Options} A self reference. + */ + androidProcess(processName) { + this.options_.androidProcess = processName; + return this; + } + + /** + * Sets whether to connect to an already-running instead of the specified + * {@linkplain #androidProcess app} instead of launching the app with a clean + * data directory. + * + * @param {boolean} useRunning Whether to connect to a running instance. + * @return {!Options} A self reference. + */ + androidUseRunningApp(useRunning) { + this.options_.androidUseRunningApp = useRunning; + return this; + } + + /** + * Sets the path to the browser's log file. This path should exist on the machine + * that will launch the browser. + * @param {string} path Path to the log file to use. + * @return {!Options} A self reference. + */ + setBrowserLogFile(path) { + this.options_.logPath = path; + return this; + } + + /** + * Sets the directory to store browser minidumps in. This option is only + * supported when the driver is running on Linux. + * @param {string} path The directory path. + * @return {!Options} A self reference. + */ + setBrowserMinidumpPath(path) { + this.options_.minidumpPath = path; + return this; + } + + /** + * Configures the browser to emulate a mobile device. For more information, refer + * to the ChromeDriver project page on [mobile emulation][em]. Configuration + * options include: + * + * - `deviceName`: The name of a pre-configured [emulated device][devem] + * - `width`: screen width, in pixels + * - `height`: screen height, in pixels + * - `pixelRatio`: screen pixel ratio + * + * __Example 1: Using a Pre-configured Device__ + * + * let options = new chrome.Options().setMobileEmulation( + * {deviceName: 'Google Nexus 5'}); + * + * let driver = chrome.Driver.createSession(options); + * + * __Example 2: Using Custom Screen Configuration__ + * + * let options = new chrome.Options().setMobileEmulation({ + * width: 360, + * height: 640, + * pixelRatio: 3.0 + * }); + * + * let driver = chrome.Driver.createSession(options); + * + * + * [em]: https://chromedriver.chromium.org/mobile-emulation + * [devem]: https://developer.chrome.com/devtools/docs/device-mode + * + * @param {?({deviceName: string}| + * {width: number, height: number, pixelRatio: number})} config The + * mobile emulation configuration, or `null` to disable emulation. + * @return {!Options} A self reference. + */ + setMobileEmulation(config) { + this.options_.mobileEmulation = config; + return this; + } + + /** + * Converts this instance to its JSON wire protocol representation. Note this + * function is an implementation not intended for general use. + * + * @return {!Object} The JSON wire protocol representation of this instance. + * @suppress {checkTypes} Suppress [] access on a struct. + */ + [Symbols.serialize]() { + if (this.options_.extensions && this.options_.extensions.length) { + this.options_.extensions = + this.options_.extensions.map(function(extension) { + if (Buffer.isBuffer(extension)) { + return extension.toString('base64'); + } + return io.read(/** @type {string} */(extension)) + .then(buffer => buffer.toString('base64')); + }); + } + return super[Symbols.serialize](); + } +} + + +/** + * Creates a new WebDriver client for Chromium-based browsers. + */ +class Driver extends webdriver.WebDriver { + /** + * Creates a new session with the WebDriver server. + * + * @param {(Capabilities|Options)=} opt_config The configuration options. + * @param {(remote.DriverService|http.Executor)=} opt_serviceExecutor Either + * a DriverService to use for the remote end, or a preconfigured executor + * for an externally managed endpoint. If neither is provided, the + * {@linkplain ##getDefaultService default service} will be used by + * default. + * @return {!Driver} A new driver instance. + */ + static createSession(caps, opt_serviceExecutor) { + let executor; + let onQuit; + if (opt_serviceExecutor instanceof http.Executor) { + executor = opt_serviceExecutor; + configureExecutor(executor, this.VENDOR_COMMAND_PREFIX); + } else { + let service = opt_serviceExecutor || this.getDefaultService(); + executor = createExecutor(service.start(), this.VENDOR_COMMAND_PREFIX); + onQuit = () => service.kill(); + } + + // W3C spec requires noProxy value to be an array of strings, but Chromium + // expects a single host as a string. + let proxy = caps.get(Capability.PROXY); + if (proxy && Array.isArray(proxy.noProxy)) { + proxy.noProxy = proxy.noProxy[0]; + if (!proxy.noProxy) { + proxy.noProxy = undefined; + } + } + + return /** @type {!Driver} */(super.createSession(executor, caps, onQuit)); + } + + /** + * This function is a no-op as file detectors are not supported by this + * implementation. + * @override + */ + setFileDetector() {} + + /** + * Schedules a command to launch Chrome App with given ID. + * @param {string} id ID of the App to launch. + * @return {!Promise} A promise that will be resolved + * when app is launched. + */ + launchApp(id) { + return this.execute( + new command.Command(Command.LAUNCH_APP).setParameter('id', id)); + } + + /** + * Schedules a command to get Chromium network emulation settings. + * @return {!Promise} A promise that will be resolved when network + * emulation settings are retrievied. + */ + getNetworkConditions() { + return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS)); + } + + /** + * Schedules a command to set Chromium network emulation settings. + * + * __Sample Usage:__ + * + * driver.setNetworkConditions({ + * offline: false, + * latency: 5, // Additional latency (ms). + * download_throughput: 500 * 1024, // Maximal aggregated download throughput. + * upload_throughput: 500 * 1024 // Maximal aggregated upload throughput. + * }); + * + * @param {Object} spec Defines the network conditions to set + * @return {!Promise} A promise that will be resolved when network + * emulation settings are set. + */ + setNetworkConditions(spec) { + if (!spec || typeof spec !== 'object') { + throw TypeError('setNetworkConditions called with non-network-conditions parameter'); + } + return this.execute( + new command.Command(Command.SET_NETWORK_CONDITIONS) + .setParameter('network_conditions', spec)); + } + + /** + * Sends an arbitrary devtools command to the browser. + * + * @param {string} cmd The name of the command to send. + * @param {Object=} params The command parameters. + * @return {!Promise} A promise that will be resolved when the command + * has finished. + * @see + */ + sendDevToolsCommand(cmd, params = {}) { + return this.execute( + new command.Command(Command.SEND_DEVTOOLS_COMMAND) + .setParameter('cmd', cmd) + .setParameter('params', params)); + } + + /** + * Sends a DevTools command to change the browser's download directory. + * + * @param {string} path The desired download directory. + * @return {!Promise} A promise that will be resolved when the command + * has finished. + * @see #sendDevToolsCommand + */ + async setDownloadPath(path) { + if (!path || typeof path !== 'string') { + throw new error.InvalidArgumentError('invalid download path'); + } + const stat = await io.stat(path); + if (!stat.isDirectory()) { + throw new error.InvalidArgumentError('not a directory: ' + path); + } + return this.sendDevToolsCommand('Page.setDownloadBehavior', { + 'behavior': 'allow', + 'downloadPath': path + }); + } + + + /** + * Returns the list of cast sinks (Cast devices) available to the Chrome media router. + * + * @return {!promise.Thenable} A promise that will be resolved with an array of Strings + * containing the friendly device names of available cast sink targets. + */ + getCastSinks() { + return this.schedule( + new command.Command(Command.GET_CAST_SINKS), + 'Driver.getCastSinks()'); + } + + /** + * Selects a cast sink (Cast device) as the recipient of media router intents (connect or play). + * + * @param {String} Friendly name of the target device. + * @return {!promise.Thenable} A promise that will be resolved + * when the target device has been selected to respond further webdriver commands. + */ + setCastSinkToUse(deviceName) { + return this.schedule( + new command.Command(Command.SET_CAST_SINK_TO_USE).setParameter('sinkName', deviceName), + 'Driver.setCastSinkToUse(' + deviceName + ')'); + } + + /** + * Initiates tab mirroring for the current browser tab on the specified device. + * + * @param {String} Friendly name of the target device. + * @return {!promise.Thenable} A promise that will be resolved + * when the mirror command has been issued to the device. + */ + startCastTabMirroring(deviceName) { + return this.schedule( + new command.Command(Command.START_CAST_TAB_MIRRORING).setParameter('sinkName', deviceName), + 'Driver.startCastTabMirroring(' + deviceName + ')'); + } + + /** + * a + * + * @param {String} Friendly name of the target device. + * @return {!promise.Thenable} A promise that will be resolved + * when the mirror command has been issued to the device. + */ + getCastIssueMessage() { + return this.schedule( + new command.Command(Command.GET_CAST_ISSUE_MESSAGE), + 'Driver.getCastIssueMessage()'); + } + + /** + * Stops casting from media router to the specified device, if connected. + * + * @param {String} Friendly name of the target device. + * @return {!promise.Thenable} A promise that will be resolved + * when the stop command has been issued to the device. + */ + stopCasting(deviceName) { + return this.schedule( + new command.Command(Command.STOP_CASTING).setParameter('sinkName', deviceName), + 'Driver.stopCasting(' + deviceName + ')'); + } +} + + +// PUBLIC API + + +exports.Driver = Driver; +exports.Options = Options; +exports.ServiceBuilder = ServiceBuilder; diff --git a/javascript/node/selenium-webdriver/edge.js b/javascript/node/selenium-webdriver/edge.js index 610a22d32d7aa..9ee60036bd673 100644 --- a/javascript/node/selenium-webdriver/edge.js +++ b/javascript/node/selenium-webdriver/edge.js @@ -17,26 +17,37 @@ /** * @fileoverview Defines a {@linkplain Driver WebDriver} client for - * Microsoft's Edge web browser. Before using this module, - * you must download and install the latest - * [MicrosoftEdgeDriver](http://go.microsoft.com/fwlink/?LinkId=619687) server. - * Ensure that the MicrosoftEdgeDriver is on your - * [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29). + * Microsoft's Edge web browser. Both Edge (Chromium), and Edge Legacy (EdgeHTML) are + * supported. Before using this module, you must download and install the correct + * [WebDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) + * server. + * + * Ensure that the either MicrosoftWebDriver (EdgeHTML) or msedgedriver (Chromium) + * is on your [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29). MicrosoftWebDriver + * and Edge Legacy (EdgeHTML) will be used by default. + * + * You may use {@link Options} to specify whether Edge Chromium should be used: + + * var options = new edge.Options(); + * options.useEdgeChromium(true); + * // configure browser options ... + + * Note that Chromium-specific {@link Options} will be ignored when using Edge Legacy. * * There are three primary classes exported by this module: * * 1. {@linkplain ServiceBuilder}: configures the * {@link ./remote.DriverService remote.DriverService} - * that manages the [MicrosoftEdgeDriver] child process. + * that manages the [WebDriver] child process. * * 2. {@linkplain Options}: defines configuration options for each new - * MicrosoftEdgeDriver session, such as which + * WebDriver session, such as which * {@linkplain Options#setProxy proxy} to use when starting the browser. * * 3. {@linkplain Driver}: the WebDriver client; each new instance will control * a unique browser session. * - * __Customizing the MicrosoftEdgeDriver Server__ + * __Customizing the WebDriver Server__ * * By default, every MicrosoftEdge session will use a single driver service, * which is started the first time a {@link Driver} instance is created and @@ -65,89 +76,110 @@ * operation, users should start MicrosoftEdge using the * {@link ./builder.Builder selenium-webdriver.Builder}. * - * [MicrosoftEdgeDriver]: https://msdn.microsoft.com/en-us/library/mt188085(v=vs.85).aspx + * [WebDriver (EdgeHTML)]: https://docs.microsoft.com/en-us/microsoft-edge/webdriver + * [WebDriver (Chromium)]: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium */ 'use strict'; -const fs = require('fs'); -const util = require('util'); - const http = require('./http'); const io = require('./io'); -const portprober = require('./net/portprober'); -const promise = require('./lib/promise'); const remote = require('./remote'); -const Symbols = require('./lib/symbols'); const webdriver = require('./lib/webdriver'); const {Browser, Capabilities} = require('./lib/capabilities'); +const chromium = require('./chromium'); -const EDGEDRIVER_EXE = 'MicrosoftWebDriver.exe'; - +const EDGE_CHROMIUM_BROWSER_NAME = "msedge"; +const EDGEDRIVER_LEGACY_EXE = 'MicrosoftWebDriver.exe'; +const EDGEDRIVER_CHROMIUM_EXE = + process.platform === 'win32' ? 'msedgedriver.exe' : 'msedgedriver'; /** - * _Synchronously_ attempts to locate the edge driver executable on the current - * system. + * _Synchronously_ attempts to locate the Edge driver executable + * on the current system. Searches for the legacy MicrosoftWebDriver by default. * + * @param {string=} browserName Name of the Edge driver executable to locate. + * May be either 'msedge' to locate the Edge Chromium driver, or 'MicrosoftEdge' to + * locate the Edge Legacy driver. If omitted, will attempt to locate Edge Legacy. * @return {?string} the located executable, or `null`. */ -function locateSynchronously() { +function locateSynchronously(browserName) { + browserName = browserName || Browser.EDGE; + + if (browserName === EDGE_CHROMIUM_BROWSER_NAME) { + return io.findInPath(EDGEDRIVER_CHROMIUM_EXE, true); + } + return process.platform === 'win32' - ? io.findInPath(EDGEDRIVER_EXE, true) : null; + ? io.findInPath(EDGEDRIVER_LEGACY_EXE, true) : null; } /** - * Class for managing MicrosoftEdgeDriver specific options. + * Class for managing Edge specific options. */ -class Options extends Capabilities { +class Options extends chromium.Options { + static USE_EDGE_CHROMIUM = 'ms:edgeChromium'; /** - * @param {(Capabilities|Map|Object)=} other Another set of - * capabilities to initialize this instance from. + * Instruct the EdgeDriver to use Edge Chromium if true. + * Otherwise, use Edge Legacy (EdgeHTML). Defaults to using Edge Legacy. + * + * @param {boolean} useEdgeChromium + * @return {!Options} A self reference. */ - constructor(other = undefined) { - super(other); - this.setBrowserName(Browser.EDGE); + setEdgeChromium(useEdgeChromium) { + this.set(Options.USE_EDGE_CHROMIUM, !!useEdgeChromium); + return this; + } +} + +Options.prototype.BROWSER_NAME_VALUE = Browser.EDGE; +Options.prototype.CAPABILITY_KEY = 'ms:edgeOptions'; +Options.prototype.VENDOR_CAPABILITY_PREFIX = 'ms'; + +/** + * @param {(Capabilities|Object)=} o The options object + * @return {boolean} + */ +function useEdgeChromium(o) { + if (o instanceof Capabilities) { + return !!o.get(Options.USE_EDGE_CHROMIUM); + } + + if (o && typeof o === 'object') { + return !!o[Options.USE_EDGE_CHROMIUM]; } + + return false; } /** * Creates {@link remote.DriverService} instances that manage a - * MicrosoftEdgeDriver server in a child process. + * WebDriver server in a child process. Used for driving both + * Microsoft Edge Legacy and Chromium. A ServiceBuilder constructed + * with default parameters will launch a MicrosoftWebDriver child + * process for driving Edge Legacy. You may pass in a path to + * msedgedriver.exe to use Edge Chromium instead. */ -class ServiceBuilder extends remote.DriverService.Builder { +class ServiceBuilder extends chromium.ServiceBuilder { /** * @param {string=} opt_exe Path to the server executable to use. If omitted, - * the builder will attempt to locate the MicrosoftEdgeDriver on the current + * the builder will attempt to locate MicrosoftWebDriver on the current * PATH. * @throws {Error} If provided executable does not exist, or the - * MicrosoftEdgeDriver cannot be found on the PATH. + * MicrosoftWebDriver cannot be found on the PATH. */ constructor(opt_exe) { let exe = opt_exe || locateSynchronously(); if (!exe) { - throw Error( - 'The ' + EDGEDRIVER_EXE + ' could not be found on the current PATH. ' + - 'Please download the latest version of the MicrosoftEdgeDriver from ' + - 'https://www.microsoft.com/en-us/download/details.aspx?id=48212 and ' + - 'ensure it can be found on your PATH.'); + throw Error('The WebDriver for Edge could not be found on the current PATH. Please ' + + 'download the latest version of ' + EDGEDRIVER_LEGACY_EXE + ' from ' + + 'https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ and ' + + 'ensure it can be found on your PATH.'); } super(exe); - - // Binding to the loopback address will fail if not running with - // administrator privileges. Since we cannot test for that in script - // (or can we?), force the DriverService to use "localhost". - this.setHostname('localhost'); - } - - /** - * Enables verbose logging. - * @return {!ServiceBuilder} A self reference. - */ - enableVerboseLogging() { - return this.addArguments('--verbose'); } } @@ -157,7 +189,7 @@ var defaultService = null; /** - * Sets the default service to use for new MicrosoftEdgeDriver instances. + * Sets the default service to use for new Edge instances. * @param {!remote.DriverService} service The service to use. * @throws {Error} If the default service is currently running. */ @@ -172,10 +204,10 @@ function setDefaultService(service) { /** - * Returns the default MicrosoftEdgeDriver service. If such a service has + * Returns the default Microsoft Edge driver service. If such a service has * not been configured, one will be constructed using the default configuration - * for an MicrosoftEdgeDriver executable found on the system PATH. - * @return {!remote.DriverService} The default MicrosoftEdgeDriver service. + * for a MicrosoftWebDriver executable found on the system PATH. + * @return {!remote.DriverService} The default Microsoft Edge driver service. */ function getDefaultService() { if (!defaultService) { @@ -184,6 +216,13 @@ function getDefaultService() { return defaultService; } +function createServiceFromCapabilities(options) { + let exe; + if (useEdgeChromium(options)) { + exe = locateSynchronously(EDGE_CHROMIUM_BROWSER_NAME); + } + return new ServiceBuilder(exe).build(); +} /** * Creates a new WebDriver client for Microsoft's Edge. @@ -193,16 +232,16 @@ class Driver extends webdriver.WebDriver { * Creates a new browser session for Microsoft's Edge browser. * * @param {(Capabilities|Options)=} options The configuration options. - * @param {remote.DriverService=} service The session to use; will use - * the {@linkplain #getDefaultService default service} by default. + * @param {remote.DriverService=} service The service to use; will create + * a new Legacy or Chromium service based on {@linkplain Options} by default. * @return {!Driver} A new driver instance. */ static createSession(options, opt_service) { - let service = opt_service || getDefaultService(); + options = options || new Options(); + let service = opt_service || createServiceFromCapabilities(options); let client = service.start().then(url => new http.HttpClient(url)); let executor = new http.Executor(client); - options = options || new Options(); return /** @type {!Driver} */(super.createSession( executor, options, () => service.kill())); } diff --git a/javascript/node/selenium-webdriver/test/lib/capabilities_test.js b/javascript/node/selenium-webdriver/test/lib/capabilities_test.js index 514f5a86f2d76..4de93d4db7764 100644 --- a/javascript/node/selenium-webdriver/test/lib/capabilities_test.js +++ b/javascript/node/selenium-webdriver/test/lib/capabilities_test.js @@ -19,11 +19,11 @@ const Capabilities = require('../../lib/capabilities').Capabilities; const Symbols = require('../../lib/symbols'); -const chrome = require('../chrome'); +const chrome = require('../../chrome'); const assert = require('assert'); const fs = require('fs'); -const io = require('../io'); +const io = require('../../io'); describe('Capabilities', function() { it('can set and unset a capability', function() { diff --git a/javascript/node/selenium-webdriver/testing/index.js b/javascript/node/selenium-webdriver/testing/index.js index ba463b9dd4eb8..788a1c948d259 100644 --- a/javascript/node/selenium-webdriver/testing/index.js +++ b/javascript/node/selenium-webdriver/testing/index.js @@ -116,6 +116,7 @@ function getAvailableBrowsers() { let targets = [ [chrome.locateSynchronously, Browser.CHROME], [edge.locateSynchronously, Browser.EDGE], + [() => edge.locateSynchronously("msedge"), Browser.EDGE, { "ms:edgeChromium": true }], [firefox.locateSynchronously, Browser.FIREFOX], [ie.locateSynchronously, Browser.IE], [safari.locateSynchronously, Browser.SAFARI], @@ -125,9 +126,10 @@ function getAvailableBrowsers() { for (let pair of targets) { const fn = pair[0]; const name = pair[1]; + const capabilities = pair[2]; if (fn()) { info(`... located ${name}`); - availableBrowsers.push({name}); + availableBrowsers.push({name, capabilities}); } } @@ -284,6 +286,11 @@ class Environment { const realBuild = builder.build; builder.build = function() { builder.forBrowser(browser.name, browser.version, browser.platform); + + if (browser.capabilities) { + builder.getCapabilities().merge(browser.capabilities); + } + if (typeof urlOrServer === 'string') { builder.usingServer(urlOrServer); } else if (urlOrServer) {