selenium-webdriver
Version:
The official WebDriver JavaScript bindings from the Selenium project
367 lines (312 loc) • 12 kB
JavaScript
// 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.
;
var AdmZip = require('adm-zip'),
fs = require('fs'),
path = require('path'),
url = require('url'),
util = require('util');
var _base = require('../_base'),
webdriver = require('../'),
promise = require('../').promise,
httpUtil = require('../http/util'),
exec = require('../io/exec'),
net = require('../net'),
portprober = require('../net/portprober');
/**
* Configuration options for a DriverService instance.
*
* - `loopback` - Whether the service should only be accessed on this host's
* loopback address.
* - `port` - The port to start the server on (must be > 0). If the port is
* provided as a promise, the service will wait for the promise to resolve
* before starting.
* - `args` - The arguments to pass to the service. If a promise is provided,
* the service will wait for it to resolve before starting.
* - `path` - The base path on the server for the WebDriver wire protocol
* (e.g. '/wd/hub'). Defaults to '/'.
* - `env` - The environment variables that should be visible to the server
* process. Defaults to inheriting the current process's environment.
* - `stdio` - IO configuration for the spawned server process. For more
* information, refer to the documentation of `child_process.spawn`.
*
* @typedef {{
* port: (number|!webdriver.promise.Promise.<number>),
* args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),
* path: (string|undefined),
* env: (!Object.<string, string>|undefined),
* stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
* }}
*/
var ServiceOptions;
/**
* Manages the life and death of a native executable WebDriver server.
*
* It is expected that the driver server implements the
* https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.
* Furthermore, the managed server should support multiple concurrent sessions,
* so that this class may be reused for multiple clients.
*
* @param {string} executable Path to the executable to run.
* @param {!ServiceOptions} options Configuration options for the service.
* @constructor
*/
function DriverService(executable, options) {
/** @private {string} */
this.executable_ = executable;
/** @private {boolean} */
this.loopbackOnly_ = !!options.loopback;
/** @private {(number|!webdriver.promise.Promise.<number>)} */
this.port_ = options.port;
/**
* @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}
*/
this.args_ = options.args;
/** @private {string} */
this.path_ = options.path || '/';
/** @private {!Object.<string, string>} */
this.env_ = options.env || process.env;
/** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
this.stdio_ = options.stdio || 'ignore';
/**
* A promise for the managed subprocess, or null if the server has not been
* started yet. This promise will never be rejected.
* @private {promise.Promise.<!exec.Command>}
*/
this.command_ = null;
/**
* Promise that resolves to the server's address or null if the server has
* not been started. This promise will be rejected if the server terminates
* before it starts accepting WebDriver requests.
* @private {promise.Promise.<string>}
*/
this.address_ = null;
}
/**
* The default amount of time, in milliseconds, to wait for the server to
* start.
* @type {number}
*/
DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
/**
* @return {!webdriver.promise.Promise.<string>} A promise that resolves to
* the server's address.
* @throws {Error} If the server has not been started.
*/
DriverService.prototype.address = function() {
if (this.address_) {
return this.address_;
}
throw Error('Server has not been started.');
};
/**
* Returns whether the underlying process is still running. This does not take
* into account whether the process is in the process of shutting down.
* @return {boolean} Whether the underlying service process is running.
*/
DriverService.prototype.isRunning = function() {
return !!this.address_;
};
/**
* Starts the server if it is not already running.
* @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
* server to start accepting requests. Defaults to 30 seconds.
* @return {!promise.Promise.<string>} A promise that will resolve
* to the server's base URL when it has started accepting requests. If the
* timeout expires before the server has started, the promise will be
* rejected.
*/
DriverService.prototype.start = function(opt_timeoutMs) {
if (this.address_) {
return this.address_;
}
var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
var self = this;
this.command_ = promise.defer();
this.address_ = promise.defer();
this.address_.fulfill(promise.when(this.port_, function(port) {
if (port <= 0) {
throw Error('Port must be > 0: ' + port);
}
return promise.when(self.args_, function(args) {
var command = exec(self.executable_, {
args: args,
env: self.env_,
stdio: self.stdio_
});
self.command_.fulfill(command);
var earlyTermination = command.result().then(function(result) {
var error = result.code == null ?
Error('Server was killed with ' + result.signal) :
Error('Server terminated early with status ' + result.code);
self.address_.reject(error);
self.address_ = null;
self.command_ = null;
throw error;
});
var serverUrl = url.format({
protocol: 'http',
hostname: !self.loopbackOnly_ && net.getAddress() ||
net.getLoopbackAddress(),
port: port,
pathname: self.path_
});
return new promise.Promise(function(fulfill, reject) {
var ready = httpUtil.waitForServer(serverUrl, timeout)
.then(fulfill, reject);
earlyTermination.thenCatch(function(e) {
ready.cancel(e);
reject(Error(e.message));
});
}).then(function() {
return serverUrl;
});
});
}));
return this.address_;
};
/**
* Stops the service if it is not currently running. This function will kill
* the server immediately. To synchronize with the active control flow, use
* {@link #stop()}.
* @return {!webdriver.promise.Promise} A promise that will be resolved when
* the server has been stopped.
*/
DriverService.prototype.kill = function() {
if (!this.address_ || !this.command_) {
return promise.fulfilled(); // Not currently running.
}
return this.command_.then(function(command) {
command.kill('SIGTERM');
});
};
/**
* Schedules a task in the current control flow to stop the server if it is
* currently running.
* @return {!webdriver.promise.Promise} A promise that will be resolved when
* the server has been stopped.
*/
DriverService.prototype.stop = function() {
return promise.controlFlow().execute(this.kill.bind(this));
};
/**
* Manages the life and death of the
* <a href="http://selenium-release.storage.googleapis.com/index.html">
* standalone Selenium server</a>.
*
* @param {string} jar Path to the Selenium server jar.
* @param {SeleniumServer.Options=} opt_options Configuration options for the
* server.
* @throws {Error} If the path to the Selenium jar is not specified or if an
* invalid port is specified.
* @constructor
* @extends {DriverService}
*/
function SeleniumServer(jar, opt_options) {
if (!jar) {
throw Error('Path to the Selenium jar not specified');
}
var options = opt_options || {};
if (options.port < 0) {
throw Error('Port must be >= 0: ' + options.port);
}
var port = options.port || portprober.findFreePort();
var args = promise.when(options.jvmArgs || [], function(jvmArgs) {
return promise.when(options.args || [], function(args) {
return promise.when(port, function(port) {
return jvmArgs.concat(['-jar', jar, '-port', port]).concat(args);
});
});
});
DriverService.call(this, 'java', {
port: port,
args: args,
path: '/wd/hub',
env: options.env,
stdio: options.stdio
});
}
util.inherits(SeleniumServer, DriverService);
/**
* Options for the Selenium server:
*
* - `port` - The port to start the server on (must be > 0). If the port is
* provided as a promise, the service will wait for the promise to resolve
* before starting.
* - `args` - The arguments to pass to the service. If a promise is provided,
* the service will wait for it to resolve before starting.
* - `jvmArgs` - The arguments to pass to the JVM. If a promise is provided,
* the service will wait for it to resolve before starting.
* - `env` - The environment variables that should be visible to the server
* process. Defaults to inheriting the current process's environment.
* - `stdio` - IO configuration for the spawned server process. For more
* information, refer to the documentation of `child_process.spawn`.
*
* @typedef {{
* port: (number|!webdriver.promise.Promise.<number>),
* args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),
* jvmArgs: (!Array.<string>|
* !webdriver.promise.Promise.<!Array.<string>>|
* undefined),
* env: (!Object.<string, string>|undefined),
* stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
* }}
*/
SeleniumServer.Options;
/**
* A {@link webdriver.FileDetector} that may be used when running
* against a remote
* [Selenium server](http://selenium-release.storage.googleapis.com/index.html).
*
* When a file path on the local machine running this script is entered with
* {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector
* will transfer the specified file to the Selenium server's host; the sendKeys
* command will be updated to use the transfered file's path.
*
* __Note:__ This class depends on a non-standard command supported on the
* Java Selenium server. The file detector will fail if used with a server that
* only supports standard WebDriver commands (such as the ChromeDriver).
*
* @constructor
* @extends {webdriver.FileDetector}
* @final
*/
var FileDetector = function() {};
util.inherits(webdriver.FileDetector, FileDetector);
/** @override */
FileDetector.prototype.handleFile = function(driver, filePath) {
return promise.checkedNodeCall(fs.stat, filePath).then(function(stats) {
if (stats.isDirectory()) {
throw TypeError('Uploading directories is not supported: ' + filePath);
}
var zip = new AdmZip();
zip.addLocalFile(filePath);
var command = new webdriver.Command(webdriver.CommandName.UPLOAD_FILE)
.setParameter('file', zip.toBuffer().toString('base64'));
return driver.schedule(command,
'remote.FileDetector.handleFile(' + filePath + ')');
}, function(err) {
if (err.code === 'ENOENT') {
return filePath; // Not a file; return original input.
}
throw err;
});
};
// PUBLIC API
exports.DriverService = DriverService;
exports.FileDetector = FileDetector;
exports.SeleniumServer = SeleniumServer;