aboutsummaryrefslogtreecommitdiff
path: root/node_modules/selenium-webdriver/safari.js
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-11-03 01:33:53 +0100
committerFlorian Dold <florian.dold@gmail.com>2016-11-03 01:33:53 +0100
commitd1291f67551c58168af43698a359cb5ddfd266b0 (patch)
tree55a13ed29fe1915e3f42f1b1b7038dafa2e975a7 /node_modules/selenium-webdriver/safari.js
parentd0a0695fb5d34996850723f7d4b1b59c3df909c2 (diff)
node_modules
Diffstat (limited to 'node_modules/selenium-webdriver/safari.js')
-rw-r--r--node_modules/selenium-webdriver/safari.js700
1 files changed, 700 insertions, 0 deletions
diff --git a/node_modules/selenium-webdriver/safari.js b/node_modules/selenium-webdriver/safari.js
new file mode 100644
index 000000000..bbfff06e8
--- /dev/null
+++ b/node_modules/selenium-webdriver/safari.js
@@ -0,0 +1,700 @@
+// 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 a WebDriver client for Safari.
+ *
+ *
+ * __Testing Older Versions of Safari__
+ *
+ * To test versions of Safari prior to Safari 10.0, you must install the
+ * [latest version](http://selenium-release.storage.googleapis.com/index.html)
+ * of the SafariDriver browser extension; using Safari for normal browsing is
+ * not recommended once the extension has been installed. You can, and should,
+ * disable the extension when the browser is not being used with WebDriver.
+ *
+ * You must also enable the use of legacy driver using the {@link Options} class.
+ *
+ * let options = new safari.Options()
+ * .useLegacyDriver(true);
+ *
+ * let driver = new (require('selenium-webdriver')).Builder()
+ * .forBrowser('safari')
+ * .setSafariOptions(options)
+ * .build();
+ */
+
+'use strict';
+
+const events = require('events');
+const fs = require('fs');
+const http = require('http');
+const path = require('path');
+const url = require('url');
+const util = require('util');
+const ws = require('ws');
+
+const io = require('./io');
+const exec = require('./io/exec');
+const isDevMode = require('./lib/devmode');
+const Capabilities = require('./lib/capabilities').Capabilities;
+const Capability = require('./lib/capabilities').Capability;
+const command = require('./lib/command');
+const error = require('./lib/error');
+const logging = require('./lib/logging');
+const promise = require('./lib/promise');
+const Session = require('./lib/session').Session;
+const Symbols = require('./lib/symbols');
+const webdriver = require('./lib/webdriver');
+const portprober = require('./net/portprober');
+const remote = require('./remote');
+const http_ = require('./http');
+
+
+/** @const */
+const CLIENT_PATH = isDevMode
+ ? path.join(__dirname,
+ '../../../buck-out/gen/javascript/safari-driver/client.js')
+ : path.join(__dirname, 'lib/safari/client.js');
+
+
+/** @const */
+const LIBRARY_DIR = (function() {
+ if (process.platform === 'darwin') {
+ return path.join('/Users', process.env['USER'], 'Library/Safari');
+ } else if (process.platform === 'win32') {
+ return path.join(process.env['APPDATA'], 'Apple Computer', 'Safari');
+ } else {
+ return '/dev/null';
+ }
+})();
+
+
+/** @const */
+const SESSION_DATA_FILES = (function() {
+ if (process.platform === 'darwin') {
+ var libraryDir = path.join('/Users', process.env['USER'], 'Library');
+ return [
+ path.join(libraryDir, 'Caches/com.apple.Safari/Cache.db'),
+ path.join(libraryDir, 'Cookies/Cookies.binarycookies'),
+ path.join(libraryDir, 'Cookies/Cookies.plist'),
+ path.join(libraryDir, 'Safari/History.plist'),
+ path.join(libraryDir, 'Safari/LastSession.plist'),
+ path.join(libraryDir, 'Safari/LocalStorage'),
+ path.join(libraryDir, 'Safari/Databases')
+ ];
+ } else if (process.platform === 'win32') {
+ var appDataDir = path.join(process.env['APPDATA'],
+ 'Apple Computer', 'Safari');
+ var localDataDir = path.join(process.env['LOCALAPPDATA'],
+ 'Apple Computer', 'Safari');
+ return [
+ path.join(appDataDir, 'History.plist'),
+ path.join(appDataDir, 'LastSession.plist'),
+ path.join(appDataDir, 'Cookies/Cookies.plist'),
+ path.join(appDataDir, 'Cookies/Cookies.binarycookies'),
+ path.join(localDataDir, 'Cache.db'),
+ path.join(localDataDir, 'Databases'),
+ path.join(localDataDir, 'LocalStorage')
+ ];
+ } else {
+ return [];
+ }
+})();
+
+
+/** @typedef {{port: number, address: string, family: string}} */
+var Host;
+
+
+/**
+ * A basic HTTP/WebSocket server used to communicate with the legacy SafariDriver
+ * browser extension.
+ */
+class Server extends events.EventEmitter {
+ constructor() {
+ super();
+ var server = http.createServer(function(req, res) {
+ if (req.url === '/favicon.ico') {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ var query = url.parse(/** @type {string} */(req.url)).query || '';
+ if (query.indexOf('url=') == -1) {
+ var address = server.address()
+ var host = address.address + ':' + address.port;
+ res.writeHead(
+ 302, {'Location': 'http://' + host + '?url=ws://' + host});
+ res.end();
+ }
+
+ fs.readFile(CLIENT_PATH, 'utf8', function(err, data) {
+ if (err) {
+ res.writeHead(500, {'Content-Type': 'text/plain'});
+ res.end(err.stack);
+ return;
+ }
+ var content = '<!DOCTYPE html><body><script>' + data + '</script>';
+ res.writeHead(200, {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(content, 'utf8'),
+ });
+ res.end(content);
+ });
+ });
+
+ var wss = new ws.Server({server: server});
+ wss.on('connection', this.emit.bind(this, 'connection'));
+
+ /**
+ * Starts the server on a random port.
+ * @return {!Promise<Host>} A promise that will resolve with the server host
+ * when it has fully started.
+ */
+ this.start = function() {
+ if (server.address()) {
+ return Promise.resolve(server.address());
+ }
+ return portprober.findFreePort('localhost').then(function(port) {
+ return promise.checkedNodeCall(
+ server.listen.bind(server, port, 'localhost'));
+ }).then(function() {
+ return server.address();
+ });
+ };
+
+ /**
+ * Stops the server.
+ * @return {!Promise} A promise that will resolve when the server has closed
+ * all connections.
+ */
+ this.stop = function() {
+ return new Promise(fulfill => server.close(fulfill));
+ };
+
+ /**
+ * @return {Host} This server's host info.
+ * @throws {Error} If the server is not running.
+ */
+ this.address = function() {
+ var addr = server.address();
+ if (!addr) {
+ throw Error('There server is not running!');
+ }
+ return addr;
+ };
+ }
+}
+
+
+/**
+ * @return {!Promise<string>} A promise that will resolve with the path
+ * to Safari on the current system.
+ */
+function findSafariExecutable() {
+ switch (process.platform) {
+ case 'darwin':
+ return Promise.resolve('/Applications/Safari.app/Contents/MacOS/Safari');
+
+ case 'win32':
+ var files = [
+ process.env['PROGRAMFILES'] || '\\Program Files',
+ process.env['PROGRAMFILES(X86)'] || '\\Program Files (x86)'
+ ].map(function(prefix) {
+ return path.join(prefix, 'Safari\\Safari.exe');
+ });
+ return io.exists(files[0]).then(function(exists) {
+ return exists ? files[0] : io.exists(files[1]).then(function(exists) {
+ if (exists) {
+ return files[1];
+ }
+ throw Error('Unable to find Safari on the current system');
+ });
+ });
+
+ default:
+ return Promise.reject(
+ Error('Safari is not supported on the current platform: ' +
+ process.platform));
+ }
+}
+
+
+/**
+ * @param {string} serverUrl The URL to connect to.
+ * @return {!Promise<string>} A promise for the path to a file that Safari can
+ * open on start-up to trigger a new connection to the WebSocket server.
+ */
+function createConnectFile(serverUrl) {
+ return io.tmpFile({postfix: '.html'}).then(function(f) {
+ let contents =
+ `<!DOCTYPE html><script>window.location = "${serverUrl}";</script>`;
+ return io.write(f, contents).then(() => f);
+ });
+}
+
+
+/**
+ * Deletes all session data files if so desired.
+ * @param {!Object} desiredCapabilities .
+ * @return {!Array<!Promise>} A list of promises for the deleted files.
+ */
+function cleanSession(desiredCapabilities) {
+ if (!desiredCapabilities) {
+ return [];
+ }
+ var options = desiredCapabilities[OPTIONS_CAPABILITY_KEY];
+ if (!options) {
+ return [];
+ }
+ if (!options['cleanSession']) {
+ return [];
+ }
+ return SESSION_DATA_FILES.map(function(file) {
+ return io.unlink(file);
+ });
+}
+
+
+/** @return {string} . */
+function getRandomString() {
+ let seed = Date.now();
+ return Math.floor(Math.random() * seed).toString(36)
+ + Math.abs(Math.floor(Math.random() * seed) ^ Date.now()).toString(36);
+}
+
+
+/**
+ * @implements {command.Executor}
+ */
+class CommandExecutor {
+ constructor() {
+ this.server_ = null;
+
+ /** @private {ws.WebSocket} */
+ this.socket_ = null;
+
+ /** @private {?string} 8*/
+ this.sessionId_ = null;
+
+ /** @private {Promise<!exec.Command>} */
+ this.safari_ = null;
+
+ /** @private {!logging.Logger} */
+ this.log_ = logging.getLogger('webdriver.safari');
+ }
+
+ /** @override */
+ execute(cmd) {
+ var self = this;
+ return new promise.Promise(function(fulfill, reject) {
+ var safariCommand = JSON.stringify({
+ 'origin': 'webdriver',
+ 'type': 'command',
+ 'command': {
+ 'id': getRandomString(),
+ 'name': cmd.getName(),
+ 'parameters': cmd.getParameters()
+ }
+ });
+
+ switch (cmd.getName()) {
+ case command.Name.NEW_SESSION:
+ self.startSafari_(cmd)
+ .then(() => self.sendCommand_(safariCommand))
+ .then(caps => new Session(self.sessionId(), caps))
+ .then(fulfill, reject);
+ break;
+
+ case command.Name.DESCRIBE_SESSION:
+ self.sendCommand_(safariCommand)
+ .then(caps => new Session(self.sessionId(), caps))
+ .then(fulfill, reject);
+ break;
+
+ case command.Name.QUIT:
+ self.destroySession_().then(() => fulfill(null), reject);
+ break;
+
+ default:
+ self.sendCommand_(safariCommand).then(fulfill, reject);
+ break;
+ }
+ });
+ }
+
+ /**
+ * @return {string} The static session ID for this executor's current
+ * connection.
+ */
+ sessionId() {
+ if (!this.sessionId_) {
+ throw Error('not currently connected')
+ }
+ return this.sessionId_;
+ }
+
+ /**
+ * @param {string} data .
+ * @return {!promise.Promise} .
+ * @private
+ */
+ sendCommand_(data) {
+ let self = this;
+ return new promise.Promise(function(fulfill, reject) {
+ // TODO: support reconnecting with the extension.
+ if (!self.socket_) {
+ self.destroySession_().finally(function() {
+ reject(Error('The connection to the SafariDriver was closed'));
+ });
+ return;
+ }
+
+ self.log_.fine(() => '>>> ' + data);
+ self.socket_.send(data, function(err) {
+ if (err) {
+ reject(err);
+ return;
+ }
+ });
+
+ self.socket_.once('message', function(data) {
+ try {
+ self.log_.fine(() => '<<< ' + data);
+ data = JSON.parse(data);
+ } catch (ex) {
+ reject(Error('Failed to parse driver message: ' + data));
+ return;
+ }
+
+ try {
+ error.checkLegacyResponse(data['response']);
+ fulfill(data['response']['value']);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {!command.Command} command .
+ * @private
+ */
+ startSafari_(command) {
+ this.server_ = new Server();
+
+ this.safari_ = this.server_.start().then(function(address) {
+ var tasks = cleanSession(
+ /** @type {!Object} */(
+ command.getParameters()['desiredCapabilities']));
+ tasks.push(
+ findSafariExecutable(),
+ createConnectFile(
+ 'http://' + address.address + ':' + address.port));
+
+ return Promise.all(tasks).then(function(/** !Array<string> */tasks) {
+ var exe = tasks[tasks.length - 2];
+ var html = tasks[tasks.length - 1];
+ return exec(exe, {args: [html]});
+ });
+ });
+
+ return new Promise((resolve, reject) => {
+ let start = Date.now();
+ let timer = setTimeout(function() {
+ let elapsed = Date.now() - start;
+ reject(Error(
+ 'Failed to connect to the SafariDriver after ' + elapsed +
+ ' ms; Have you installed the latest extension from ' +
+ 'http://selenium-release.storage.googleapis.com/index.html?'));
+ }, 10 * 1000);
+
+ this.server_.once('connection', socket => {
+ clearTimeout(timer);
+ this.socket_ = socket;
+ this.sessionId_ = getRandomString();
+ socket.once('close', () => {
+ this.socket_ = null;
+ this.sessionId_ = null;
+ });
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Destroys the active session by stopping the WebSocket server and killing the
+ * Safari subprocess.
+ * @private
+ */
+ destroySession_() {
+ var tasks = [];
+ if (this.server_) {
+ tasks.push(this.server_.stop());
+ }
+ if (this.safari_) {
+ tasks.push(this.safari_.then(function(safari) {
+ safari.kill();
+ return safari.result();
+ }));
+ }
+ var self = this;
+ return promise.all(tasks).finally(function() {
+ self.server_ = null;
+ self.socket_ = null;
+ self.safari_ = null;
+ });
+ }
+}
+
+
+/**
+ * @return {string} .
+ * @throws {Error}
+ */
+function findSafariDriver() {
+ let exe = io.findInPath('safaridriver', true);
+ if (!exe) {
+ throw Error(
+ `The safaridriver executable could not be found on the current PATH.
+ Please ensure you are using Safari 10.0 or above.`);
+ }
+ return exe;
+}
+
+
+/**
+ * Creates {@link selenium-webdriver/remote.DriverService} instances that manage
+ * a [safaridriver] server in a child process.
+ *
+ * [safaridriver]: https://developer.apple.com/library/prerelease/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html#//apple_ref/doc/uid/TP40014305-CH11-DontLinkElementID_28
+ */
+class ServiceBuilder extends remote.DriverService.Builder {
+ /**
+ * @param {string=} opt_exe Path to the server executable to use. If omitted,
+ * the builder will attempt to locate the safaridriver on the system PATH.
+ */
+ constructor(opt_exe) {
+ super(opt_exe || findSafariDriver());
+ this.setLoopback(true); // Required.
+ }
+}
+
+
+/** @const */
+const OPTIONS_CAPABILITY_KEY = 'safari.options';
+const LEGACY_DRIVER_CAPABILITY_KEY = 'legacyDriver'
+
+
+
+/**
+ * Configuration options specific to the {@link Driver SafariDriver}.
+ */
+class Options {
+ constructor() {
+ /** @private {Object<string, *>} */
+ this.options_ = null;
+
+ /** @private {./lib/logging.Preferences} */
+ this.logPrefs_ = null;
+
+ /** @private {?./lib/capabilities.ProxyConfig} */
+ this.proxy_ = null;
+
+ /** @private {boolean} */
+ this.legacyDriver_ = false;
+ }
+
+ /**
+ * Extracts the SafariDriver specific options from the given capabilities
+ * object.
+ * @param {!Capabilities} capabilities The capabilities object.
+ * @return {!Options} The ChromeDriver options.
+ */
+ static fromCapabilities(capabilities) {
+ var options = new Options();
+
+ var o = capabilities.get(OPTIONS_CAPABILITY_KEY);
+ if (o instanceof Options) {
+ options = o;
+ } else if (o) {
+ options.setCleanSession(o.cleanSession);
+ }
+
+ if (capabilities.has(Capability.PROXY)) {
+ options.setProxy(capabilities.get(Capability.PROXY));
+ }
+
+ if (capabilities.has(Capability.LOGGING_PREFS)) {
+ options.setLoggingPrefs(capabilities.get(Capability.LOGGING_PREFS));
+ }
+
+ if (capabilities.has(LEGACY_DRIVER_CAPABILITY_KEY)) {
+ options.useLegacyDriver(capabilities.get(LEGACY_DRIVER_CAPABILITY_KEY));
+ }
+
+ return options;
+ }
+
+ /**
+ * Sets whether to force Safari to start with a clean session. Enabling this
+ * option will cause all global browser data to be deleted.
+ * @param {boolean} clean Whether to make sure the session has no cookies,
+ * cache entries, local storage, or databases.
+ * @return {!Options} A self reference.
+ */
+ setCleanSession(clean) {
+ if (!this.options_) {
+ this.options_ = {};
+ }
+ this.options_['cleanSession'] = clean;
+ return this;
+ }
+
+ /**
+ * Sets whether to use the legacy driver from the Selenium project. This option
+ * is disabled by default.
+ *
+ * @param {boolean} enable Whether to enable the legacy driver.
+ * @return {!Options} A self reference.
+ */
+ useLegacyDriver(enable) {
+ this.legacyDriver_ = enable;
+ return this;
+ }
+
+ /**
+ * Sets the logging preferences for the new session.
+ * @param {!./lib/logging.Preferences} prefs The logging preferences.
+ * @return {!Options} A self reference.
+ */
+ setLoggingPrefs(prefs) {
+ this.logPrefs_ = prefs;
+ return this;
+ }
+
+ /**
+ * Sets the proxy to use.
+ *
+ * @param {./lib/capabilities.ProxyConfig} proxy The proxy configuration to use.
+ * @return {!Options} A self reference.
+ */
+ setProxy(proxy) {
+ this.proxy_ = proxy;
+ return this;
+ }
+
+ /**
+ * Converts this options instance to a {@link Capabilities} object.
+ * @param {Capabilities=} opt_capabilities The capabilities to
+ * merge these options into, if any.
+ * @return {!Capabilities} The capabilities.
+ */
+ toCapabilities(opt_capabilities) {
+ var caps = opt_capabilities || Capabilities.safari();
+ if (this.logPrefs_) {
+ caps.set(Capability.LOGGING_PREFS, this.logPrefs_);
+ }
+ if (this.proxy_) {
+ caps.set(Capability.PROXY, this.proxy_);
+ }
+ if (this.options_) {
+ caps.set(OPTIONS_CAPABILITY_KEY, this);
+ }
+ caps.set(LEGACY_DRIVER_CAPABILITY_KEY, this.legacyDriver_);
+ return caps;
+ }
+
+ /**
+ * Converts this instance to its JSON wire protocol representation. Note this
+ * function is an implementation detail not intended for general use.
+ * @return {!Object<string, *>} The JSON wire protocol representation of this
+ * instance.
+ */
+ [Symbols.serialize]() {
+ return this.options_ || {};
+ }
+}
+
+
+/**
+ * A WebDriver client for Safari. This class should never be instantiated
+ * directly; instead, use the {@linkplain ./builder.Builder Builder}:
+ *
+ * var driver = new Builder()
+ * .forBrowser('safari')
+ * .build();
+ *
+ */
+class Driver extends webdriver.WebDriver {
+ /**
+ * @param {(Options|Capabilities)=} opt_config The configuration
+ * options for the new session.
+ * @param {promise.ControlFlow=} opt_flow The control flow to create
+ * the driver under.
+ */
+ constructor(opt_config, opt_flow) {
+ let caps,
+ executor,
+ useLegacyDriver = false,
+ onQuit = () => {};
+
+ if (opt_config instanceof Options) {
+ caps = opt_config.toCapabilities();
+ } else {
+ caps = opt_config || Capabilities.safari()
+ }
+
+ if (caps.has(LEGACY_DRIVER_CAPABILITY_KEY)) {
+ useLegacyDriver = caps.get(LEGACY_DRIVER_CAPABILITY_KEY);
+ caps.delete(LEGACY_DRIVER_CAPABILITY_KEY);
+ }
+
+ if (useLegacyDriver) {
+ executor = new CommandExecutor();
+ } else {
+ let service = new ServiceBuilder().build();
+
+ executor = new http_.Executor(
+ service.start()
+ .then(url => new http_.HttpClient(url))
+ );
+
+ onQuit = () => service.kill();
+ }
+
+ let driver = webdriver.WebDriver.createSession(executor, caps, opt_flow);
+
+ super(driver.getSession(), executor, driver.controlFlow());
+
+ /** @override */
+ this.quit = () => {
+ return super.quit().finally(onQuit);
+ };
+ }
+}
+
+
+// Public API
+
+
+exports.Driver = Driver;
+exports.Options = Options;
+exports.ServiceBuilder = ServiceBuilder;