diff options
Diffstat (limited to 'node_modules/selenium-webdriver/safari.js')
-rw-r--r-- | node_modules/selenium-webdriver/safari.js | 700 |
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; |