// 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 {@linkplain Driver WebDriver} client for the * PhantomJS web browser. By default, it is expected that the PhantomJS * executable can be located on your * [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) * * __Using a Custom PhantomJS Binary__ * * If you have PhantomJS.exe placed somewhere other than the root of your * working directory, you can build a custom Capability and attach the * executable's location to the Capability * * For example, if you're using the * [phantomjs-prebuilt](https://www.npmjs.com/package/phantomjs-prebuilt) module * from npm: * * //setup custom phantomJS capability * var phantomjs_exe = require('phantomjs-prebuilt').path; * var customPhantom = selenium.Capabilities.phantomjs(); * customPhantom.set("phantomjs.binary.path", phantomjs_exe); * //build custom phantomJS driver * var driver = new selenium.Builder(). * withCapabilities(customPhantom). * build(); * */ 'use strict'; const fs = require('fs'); const http = require('./http'), io = require('./io'), capabilities = require('./lib/capabilities'), command = require('./lib/command'), logging = require('./lib/logging'), promise = require('./lib/promise'), webdriver = require('./lib/webdriver'), portprober = require('./net/portprober'), remote = require('./remote'); /** * Name of the PhantomJS executable. * @type {string} * @const */ const PHANTOMJS_EXE = process.platform === 'win32' ? 'phantomjs.exe' : 'phantomjs'; /** * Capability that designates the location of the PhantomJS executable to use. * @type {string} * @const */ const BINARY_PATH_CAPABILITY = 'phantomjs.binary.path'; /** * Capability that designates the CLI arguments to pass to PhantomJS. * @type {string} * @const */ const CLI_ARGS_CAPABILITY = 'phantomjs.cli.args'; /** * Custom command names supported by PhantomJS. * @enum {string} */ const Command = { EXECUTE_PHANTOM_SCRIPT: 'executePhantomScript' }; /** * Finds the PhantomJS executable. * @param {string=} opt_exe Path to the executable to use. * @return {string} The located executable. * @throws {Error} If the executable cannot be found on the PATH, or if the * provided executable path does not exist. */ function findExecutable(opt_exe) { var exe = opt_exe || io.findInPath(PHANTOMJS_EXE, true); if (!exe) { throw Error( 'The PhantomJS executable could not be found on the current PATH. ' + 'Please download the latest version from ' + 'http://phantomjs.org/download.html and ensure it can be found on ' + 'your PATH. For more information, see ' + 'https://github.com/ariya/phantomjs/wiki'); } if (!fs.existsSync(exe)) { throw Error('File does not exist: ' + exe); } return exe; } /** * Maps WebDriver logging level name to those recognised by PhantomJS. * @const {!Map} */ const WEBDRIVER_TO_PHANTOMJS_LEVEL = new Map([ [logging.Level.ALL.name, 'DEBUG'], [logging.Level.DEBUG.name, 'DEBUG'], [logging.Level.INFO.name, 'INFO'], [logging.Level.WARNING.name, 'WARN'], [logging.Level.SEVERE.name, 'ERROR']]); /** * Creates a command executor with support for PhantomJS' custom commands. * @param {!Promise} url The server's URL. * @return {!command.Executor} The new command executor. */ function createExecutor(url) { let client = url.then(url => new http.HttpClient(url)); let executor = new http.Executor(client); executor.defineCommand( Command.EXECUTE_PHANTOM_SCRIPT, 'POST', '/session/:sessionId/phantom/execute'); return executor; } /** * Creates a new WebDriver client for PhantomJS. */ class Driver extends webdriver.WebDriver { /** * @param {capabilities.Capabilities=} opt_capabilities The desired * capabilities. * @param {promise.ControlFlow=} opt_flow The control flow to use, * or {@code null} to use the currently active flow. * @param {string=} opt_logFile Path to the log file for the phantomjs * executable's output. For convenience, this may be set at runtime with * the `SELENIUM_PHANTOMJS_LOG` environment variable. */ constructor(opt_capabilities, opt_flow, opt_logFile) { // TODO: add an Options class for consistency with the other driver types. var caps = opt_capabilities || capabilities.Capabilities.phantomjs(); var exe = findExecutable(caps.get(BINARY_PATH_CAPABILITY)); var args = []; var logPrefs = caps.get(capabilities.Capability.LOGGING_PREFS); if (logPrefs instanceof logging.Preferences) { logPrefs = logPrefs.toJSON(); } if (logPrefs && logPrefs[logging.Type.DRIVER]) { let level = WEBDRIVER_TO_PHANTOMJS_LEVEL.get( logPrefs[logging.Type.DRIVER]); if (level) { args.push('--webdriver-loglevel=' + level); } } opt_logFile = process.env['SELENIUM_PHANTOMJS_LOG'] || opt_logFile; if (typeof opt_logFile === 'string') { args.push('--webdriver-logfile=' + opt_logFile); } var proxy = caps.get(capabilities.Capability.PROXY); if (proxy) { switch (proxy.proxyType) { case 'manual': if (proxy.httpProxy) { args.push( '--proxy-type=http', '--proxy=' + proxy.httpProxy); console.log(args); } break; case 'pac': throw Error('PhantomJS does not support Proxy PAC files'); case 'system': args.push('--proxy-type=system'); break; case 'direct': args.push('--proxy-type=none'); break; } } args = args.concat(caps.get(CLI_ARGS_CAPABILITY) || []); var port = portprober.findFreePort(); var service = new remote.DriverService(exe, { port: port, stdio: 'inherit', args: Promise.resolve(port).then(function(port) { args.push('--webdriver=' + port); return args; }) }); var executor = createExecutor(service.start()); var driver = webdriver.WebDriver.createSession(executor, caps, opt_flow); super(driver.getSession(), executor, driver.controlFlow()); var boundQuit = this.quit.bind(this); /** @override */ this.quit = function() { let killService = () => service.kill(); return boundQuit().then(killService, killService); }; } /** * This function is a no-op as file detectors are not supported by this * implementation. * @override */ setFileDetector() {} /** * Executes a PhantomJS fragment. This method is similar to * {@link #executeScript}, except it exposes the * PhantomJS API to the injected * script. * *

The injected script will execute in the context of PhantomJS's * {@code page} variable. If a page has not been loaded before calling this * method, one will be created.

* *

Be sure to wrap callback definitions in a try/catch block, as failures * may cause future WebDriver calls to fail.

* *

Certain callbacks are used by GhostDriver (the PhantomJS WebDriver * implementation) and overriding these may cause the script to fail. It is * recommended that you check for existing callbacks before defining your own. *

* * As with {@link #executeScript}, the injected script may be defined as * a string for an anonymous function body (e.g. "return 123;"), or as a * function. If a function is provided, it will be decompiled to its original * source. Note that injecting functions is provided as a convenience to * simplify defining complex scripts. Care must be taken that the function * only references variables that will be defined in the page's scope and * that the function does not override {@code Function.prototype.toString} * (overriding toString() will interfere with how the function is * decompiled. * * @param {(string|!Function)} script The script to execute. * @param {...*} var_args The arguments to pass to the script. * @return {!promise.Promise} A promise that resolve to the * script's return value. * @template T */ executePhantomJS(script, var_args) { if (typeof script === 'function') { script = 'return (' + script + ').apply(this, arguments);'; } var args = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; return this.schedule( new command.Command(Command.EXECUTE_PHANTOM_SCRIPT) .setParameter('script', script) .setParameter('args', args), 'Driver.executePhantomJS()'); } } // PUBLIC API exports.Driver = Driver;