| // 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<string, string>} |
| */ |
| 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<string>} 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 { |
| /** |
| * Creates a new PhantomJS session. |
| * |
| * @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. |
| * @return {!Driver} A new driver reference. |
| */ |
| static createSession(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, |
| args: Promise.resolve(port).then(function(port) { |
| args.push('--webdriver=' + port); |
| return args; |
| }) |
| }); |
| |
| var executor = createExecutor(service.start()); |
| return /** @type {!Driver} */(super.createSession( |
| executor, caps, opt_flow, () => service.kill())); |
| } |
| |
| /** |
| * 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 |
| * <a href="http://phantomjs.org/api/">PhantomJS API</a> to the injected |
| * script. |
| * |
| * <p>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.</p> |
| * |
| * <p>Be sure to wrap callback definitions in a try/catch block, as failures |
| * may cause future WebDriver calls to fail.</p> |
| * |
| * <p>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. |
| * </p> |
| * |
| * 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.Thenable<T>} 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; |