UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

856 lines (794 loc) 29.3 kB
// 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). * * let chrome = require('selenium-webdriver/chrome'); * let {Builder} = require('selenium-webdriver'); * * let driver = new Builder() * .forBrowser('chrome') * .setChromeOptions(new chrome.Options()) * .build(); * * __Customizing the Chromium WebDriver Server__ <a id="custom-server"></a> * * 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); * * @module selenium-webdriver/chromium */ '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 Symbols = require('./lib/symbols') const webdriver = require('./lib/webdriver') const remote = require('./remote') const { getBinaryPaths } = require('./common/driverFinder') /** * Custom command names supported by Chromium WebDriver. * @enum {string} */ const Command = { LAUNCH_APP: 'launchApp', GET_NETWORK_CONDITIONS: 'getNetworkConditions', SET_NETWORK_CONDITIONS: 'setNetworkConditions', DELETE_NETWORK_CONDITIONS: 'deleteNetworkConditions', SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand', SEND_AND_GET_DEVTOOLS_COMMAND: 'sendAndGetDevToolsCommand', SET_PERMISSION: 'setPermission', GET_CAST_SINKS: 'getCastSinks', SET_CAST_SINK_TO_USE: 'setCastSinkToUse', START_CAST_DESKTOP_MIRRORING: 'startDesktopMirroring', 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<string>} url The server's URL. * @param vendorPrefix * @return {!command.Executor} The new command executor. */ function createExecutor(url, vendorPrefix) { const agent = new http.Agent({ keepAlive: true }) const client = url.then((url) => new http.HttpClient(url, agent)) const 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.DELETE_NETWORK_CONDITIONS, 'DELETE', '/session/:sessionId/chromium/network_conditions') executor.defineCommand(Command.SEND_DEVTOOLS_COMMAND, 'POST', '/session/:sessionId/chromium/send_command') executor.defineCommand( Command.SEND_AND_GET_DEVTOOLS_COMMAND, 'POST', '/session/:sessionId/chromium/send_command_and_get_result', ) executor.defineCommand(Command.SET_PERMISSION, 'POST', '/session/:sessionId/permissions') 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_DESKTOP_MIRRORING, 'POST', `/session/:sessionId/${vendorPrefix}/cast/start_desktop_mirroring`, ) 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 Chrome logging. * @returns {!ServiceBuilder} A self reference. */ enableChromeLogging() { return this.addArguments('--enable-chrome-logs') } /** * 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<string, ?>|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<string>)} 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 } /** * Sets the address of a Chromium remote debugging server to connect to. * Address should be of the form "{hostname|IP address}:port" * (e.g. "localhost:9222"). * * @param {string} address The address to connect to. * @return {!Options} A self reference. */ debuggerAddress(address) { this.options_.debuggerAddress = address return this } /** * 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<string>)} 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 extensions = this.options_.extensions || new Extensions() extensions.add(...args) if (extensions.length) { this.options_.extensions = extensions } 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. * - `traceCategories`: 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, * traceCategories: 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({deviceMetrics: { * 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 } /** * Sets a list of the window types that will appear when getting window * handles. For access to <webview> elements, include "webview" in the list. * @param {...(string|!Array<string>)} args The window types that will appear * when getting window handles. * @return {!Options} A self reference. */ windowTypes(...args) { let windowTypes = (this.options_.windowTypes || []).concat(...args) if (windowTypes.length) { this.options_.windowTypes = windowTypes } return this } /** * Enable bidi connection * @returns {!Capabilities} */ enableBidi() { return this.set('webSocketUrl', true) } } /** * A list of extensions to install when launching the browser. */ class Extensions { constructor() { this.extensions = [] } /** * @return {number} The length of the extensions list. */ get length() { return this.extensions.length } /** * 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. */ add(...args) { this.extensions = this.extensions.concat(...args) } /** * @return {!Object} A serialized representation of this Extensions object. */ [Symbols.serialize]() { return this.extensions.map(function (extension) { if (Buffer.isBuffer(extension)) { return extension.toString('base64') } return io.read(/** @type {string} */ (extension)).then((buffer) => buffer.toString('base64')) }) } } /** * 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)=} caps 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. * @param vendorPrefix Either 'goog' or 'ms' * @param vendorCapabilityKey Either 'goog:chromeOptions' or 'ms:edgeOptions' * @return {!Driver} A new driver instance. */ static createSession(caps, opt_serviceExecutor, vendorPrefix = '', vendorCapabilityKey = '') { let executor let onQuit if (opt_serviceExecutor instanceof http.Executor) { executor = opt_serviceExecutor configureExecutor(executor, vendorPrefix) } else { let service = opt_serviceExecutor || this.getDefaultService() if (!service.getExecutable()) { const { driverPath, browserPath } = getBinaryPaths(caps) service.setExecutable(driverPath) if (browserPath) { const vendorOptions = caps.get(vendorCapabilityKey) if (vendorOptions) { vendorOptions['binary'] = browserPath caps.set(vendorCapabilityKey, vendorOptions) } else { caps.set(vendorCapabilityKey, { binary: browserPath }) } caps.delete(Capability.BROWSER_VERSION) } } onQuit = () => service.kill() executor = createExecutor(service.start(), vendorPrefix) } // 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<void>} 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 retrieved. */ getNetworkConditions() { return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS)) } /** * Schedules a command to delete Chromium network emulation settings. * @return {!Promise} A promise that will be resolved when network * emulation settings have been deleted. */ deleteNetworkConditions() { return this.execute(new command.Command(Command.DELETE_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<void>} 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<void>} A promise that will be resolved when the command * has finished. * @see <https://chromedevtools.github.io/devtools-protocol/> */ sendDevToolsCommand(cmd, params = {}) { return this.execute( new command.Command(Command.SEND_DEVTOOLS_COMMAND).setParameter('cmd', cmd).setParameter('params', params), ) } /** * Sends an arbitrary devtools command to the browser and get the result. * * @param {string} cmd The name of the command to send. * @param {Object=} params The command parameters. * @return {!Promise<string>} A promise that will be resolved when the command * has finished. * @see <https://chromedevtools.github.io/devtools-protocol/> */ sendAndGetDevToolsCommand(cmd, params = {}) { return this.execute( new command.Command(Command.SEND_AND_GET_DEVTOOLS_COMMAND) .setParameter('cmd', cmd) .setParameter('params', params), ) } /** * Set a permission state to the given value. * * @param {string} name A name of the permission to update. * @param {("granted"|"denied"|"prompt")} state State to set permission to. * @returns {!Promise<Object>} A promise that will be resolved when the * command has finished. * @see <https://w3c.github.io/permissions/#permission-registry> for valid * names */ setPermission(name, state) { return this.execute( new command.Command(Command.SET_PERMISSION).setParameter('descriptor', { name }).setParameter('state', state), ) } /** * Sends a DevTools command to change the browser's download directory. * * @param {string} path The desired download directory. * @return {!Promise<void>} 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<void>} A promise that will be resolved with an array of Strings * containing the friendly device names of available cast sink targets. */ getCastSinks() { return this.execute(new command.Command(Command.GET_CAST_SINKS)) } /** * Selects a cast sink (Cast device) as the recipient of media router intents (connect or play). * * @param {String} deviceName name of the target device. * @return {!promise.Thenable<void>} A promise that will be resolved * when the target device has been selected to respond further webdriver commands. */ setCastSinkToUse(deviceName) { return this.execute(new command.Command(Command.SET_CAST_SINK_TO_USE).setParameter('sinkName', deviceName)) } /** * Initiates desktop mirroring for the current browser tab on the specified device. * * @param {String} deviceName name of the target device. * @return {!promise.Thenable<void>} A promise that will be resolved * when the mirror command has been issued to the device. */ startDesktopMirroring(deviceName) { return this.execute(new command.Command(Command.START_CAST_DESKTOP_MIRRORING).setParameter('sinkName', deviceName)) } /** * Initiates tab mirroring for the current browser tab on the specified device. * * @param {String} deviceName name of the target device. * @return {!promise.Thenable<void>} A promise that will be resolved * when the mirror command has been issued to the device. */ startCastTabMirroring(deviceName) { return this.execute(new command.Command(Command.START_CAST_TAB_MIRRORING).setParameter('sinkName', deviceName)) } /** * Returns an error message when there is any issue in a Cast session. * @return {!promise.Thenable<void>} A promise that will be resolved * when the mirror command has been issued to the device. */ getCastIssueMessage() { return this.execute(new command.Command(Command.GET_CAST_ISSUE_MESSAGE)) } /** * Stops casting from media router to the specified device, if connected. * * @param {String} deviceName name of the target device. * @return {!promise.Thenable<void>} A promise that will be resolved * when the stop command has been issued to the device. */ stopCasting(deviceName) { return this.execute(new command.Command(Command.STOP_CASTING).setParameter('sinkName', deviceName)) } } // PUBLIC API module.exports = { Driver, Options, ServiceBuilder, }