| 1 | // Licensed to the Software Freedom Conservancy (SFC) under one |
| 2 | // or more contributor license agreements. See the NOTICE file |
| 3 | // distributed with this work for additional information |
| 4 | // regarding copyright ownership. The SFC licenses this file |
| 5 | // to you under the Apache License, Version 2.0 (the |
| 6 | // "License"); you may not use this file except in compliance |
| 7 | // with the License. You may obtain a copy of the License at |
| 8 | // |
| 9 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | // |
| 11 | // Unless required by applicable law or agreed to in writing, |
| 12 | // software distributed under the License is distributed on an |
| 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 14 | // KIND, either express or implied. See the License for the |
| 15 | // specific language governing permissions and limitations |
| 16 | // under the License. |
| 17 | |
| 18 | 'use strict'; |
| 19 | |
| 20 | var fs = require('fs'), |
| 21 | util = require('util'); |
| 22 | |
| 23 | var webdriver = require('./index'), |
| 24 | executors = require('./executors'), |
| 25 | http = require('./http'), |
| 26 | io = require('./io'), |
| 27 | portprober = require('./net/portprober'), |
| 28 | remote = require('./remote'); |
| 29 | |
| 30 | |
| 31 | /** |
| 32 | * Name of the PhantomJS executable. |
| 33 | * @type {string} |
| 34 | * @const |
| 35 | */ |
| 36 | var PHANTOMJS_EXE = |
| 37 | process.platform === 'win32' ? 'phantomjs.exe' : 'phantomjs'; |
| 38 | |
| 39 | |
| 40 | /** |
| 41 | * Capability that designates the location of the PhantomJS executable to use. |
| 42 | * @type {string} |
| 43 | * @const |
| 44 | */ |
| 45 | var BINARY_PATH_CAPABILITY = 'phantomjs.binary.path'; |
| 46 | |
| 47 | |
| 48 | /** |
| 49 | * Capability that designates the CLI arguments to pass to PhantomJS. |
| 50 | * @type {string} |
| 51 | * @const |
| 52 | */ |
| 53 | var CLI_ARGS_CAPABILITY = 'phantomjs.cli.args'; |
| 54 | |
| 55 | |
| 56 | /** |
| 57 | * Default log file to use if one is not specified through CLI args. |
| 58 | * @type {string} |
| 59 | * @const |
| 60 | */ |
| 61 | var DEFAULT_LOG_FILE = 'phantomjsdriver.log'; |
| 62 | |
| 63 | |
| 64 | /** |
| 65 | * Custom command names supported by PhantomJS. |
| 66 | * @enum {string} |
| 67 | */ |
| 68 | var Command = { |
| 69 | EXECUTE_PHANTOM_SCRIPT: 'executePhantomScript' |
| 70 | }; |
| 71 | |
| 72 | |
| 73 | /** |
| 74 | * Finds the PhantomJS executable. |
| 75 | * @param {string=} opt_exe Path to the executable to use. |
| 76 | * @return {string} The located executable. |
| 77 | * @throws {Error} If the executable cannot be found on the PATH, or if the |
| 78 | * provided executable path does not exist. |
| 79 | */ |
| 80 | function findExecutable(opt_exe) { |
| 81 | var exe = opt_exe || io.findInPath(PHANTOMJS_EXE, true); |
| 82 | if (!exe) { |
| 83 | throw Error( |
| 84 | 'The PhantomJS executable could not be found on the current PATH. ' + |
| 85 | 'Please download the latest version from ' + |
| 86 | 'http://phantomjs.org/download.html and ensure it can be found on ' + |
| 87 | 'your PATH. For more information, see ' + |
| 88 | 'https://github.com/ariya/phantomjs/wiki'); |
| 89 | } |
| 90 | if (!fs.existsSync(exe)) { |
| 91 | throw Error('File does not exist: ' + exe); |
| 92 | } |
| 93 | return exe; |
| 94 | } |
| 95 | |
| 96 | |
| 97 | /** |
| 98 | * Maps WebDriver logging level name to those recognised by PhantomJS. |
| 99 | * @type {!Object.<string, string>} |
| 100 | * @const |
| 101 | */ |
| 102 | var WEBDRIVER_TO_PHANTOMJS_LEVEL = (function() { |
| 103 | var map = {}; |
| 104 | map[webdriver.logging.Level.ALL.name] = 'DEBUG'; |
| 105 | map[webdriver.logging.Level.DEBUG.name] = 'DEBUG'; |
| 106 | map[webdriver.logging.Level.INFO.name] = 'INFO'; |
| 107 | map[webdriver.logging.Level.WARNING.name] = 'WARN'; |
| 108 | map[webdriver.logging.Level.SEVERE.name] = 'ERROR'; |
| 109 | return map; |
| 110 | })(); |
| 111 | |
| 112 | |
| 113 | /** |
| 114 | * Creates a command executor with support for PhantomJS' custom commands. |
| 115 | * @param {!webdriver.promise.Promise<string>} url The server's URL. |
| 116 | * @return {!webdriver.CommandExecutor} The new command executor. |
| 117 | */ |
| 118 | function createExecutor(url) { |
| 119 | return new executors.DeferredExecutor(url.then(function(url) { |
| 120 | var client = new http.HttpClient(url); |
| 121 | var executor = new http.Executor(client); |
| 122 | |
| 123 | executor.defineCommand( |
| 124 | Command.EXECUTE_PHANTOM_SCRIPT, |
| 125 | 'POST', '/session/:sessionId/phantom/execute'); |
| 126 | |
| 127 | return executor; |
| 128 | })); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Creates a new WebDriver client for PhantomJS. |
| 133 | * |
| 134 | * @param {webdriver.Capabilities=} opt_capabilities The desired capabilities. |
| 135 | * @param {webdriver.promise.ControlFlow=} opt_flow The control flow to use, or |
| 136 | * {@code null} to use the currently active flow. |
| 137 | * @constructor |
| 138 | * @extends {webdriver.WebDriver} |
| 139 | */ |
| 140 | var Driver = function(opt_capabilities, opt_flow) { |
| 141 | var capabilities = opt_capabilities || webdriver.Capabilities.phantomjs(); |
| 142 | var exe = findExecutable(capabilities.get(BINARY_PATH_CAPABILITY)); |
| 143 | var args = ['--webdriver-logfile=' + DEFAULT_LOG_FILE]; |
| 144 | |
| 145 | var logPrefs = capabilities.get(webdriver.Capability.LOGGING_PREFS); |
| 146 | if (logPrefs instanceof webdriver.logging.Preferences) { |
| 147 | logPrefs = logPrefs.toJSON(); |
| 148 | } |
| 149 | |
| 150 | if (logPrefs && logPrefs[webdriver.logging.Type.DRIVER]) { |
| 151 | var level = WEBDRIVER_TO_PHANTOMJS_LEVEL[ |
| 152 | logPrefs[webdriver.logging.Type.DRIVER]]; |
| 153 | if (level) { |
| 154 | args.push('--webdriver-loglevel=' + level); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | var proxy = capabilities.get(webdriver.Capability.PROXY); |
| 159 | if (proxy) { |
| 160 | switch (proxy.proxyType) { |
| 161 | case 'manual': |
| 162 | if (proxy.httpProxy) { |
| 163 | args.push( |
| 164 | '--proxy-type=http', |
| 165 | '--proxy=http://' + proxy.httpProxy); |
| 166 | } |
| 167 | break; |
| 168 | case 'pac': |
| 169 | throw Error('PhantomJS does not support Proxy PAC files'); |
| 170 | case 'system': |
| 171 | args.push('--proxy-type=system'); |
| 172 | break; |
| 173 | case 'direct': |
| 174 | args.push('--proxy-type=none'); |
| 175 | break; |
| 176 | } |
| 177 | } |
| 178 | args = args.concat(capabilities.get(CLI_ARGS_CAPABILITY) || []); |
| 179 | |
| 180 | var port = portprober.findFreePort(); |
| 181 | var service = new remote.DriverService(exe, { |
| 182 | port: port, |
| 183 | args: webdriver.promise.when(port, function(port) { |
| 184 | args.push('--webdriver=' + port); |
| 185 | return args; |
| 186 | }) |
| 187 | }); |
| 188 | |
| 189 | var executor = createExecutor(service.start()); |
| 190 | var driver = webdriver.WebDriver.createSession( |
| 191 | executor, capabilities, opt_flow); |
| 192 | |
| 193 | webdriver.WebDriver.call( |
| 194 | this, driver.getSession(), executor, driver.controlFlow()); |
| 195 | |
| 196 | var boundQuit = this.quit.bind(this); |
| 197 | |
| 198 | /** @override */ |
| 199 | this.quit = function() { |
| 200 | return boundQuit().thenFinally(service.kill.bind(service)); |
| 201 | }; |
| 202 | }; |
| 203 | util.inherits(Driver, webdriver.WebDriver); |
| 204 | |
| 205 | |
| 206 | /** |
| 207 | * This function is a no-op as file detectors are not supported by this |
| 208 | * implementation. |
| 209 | * @override |
| 210 | */ |
| 211 | Driver.prototype.setFileDetector = function() { |
| 212 | }; |
| 213 | |
| 214 | |
| 215 | /** |
| 216 | * Executes a PhantomJS fragment. This method is similar to |
| 217 | * {@link #executeScript}, except it exposes the |
| 218 | * <a href="http://phantomjs.org/api/">PhantomJS API</a> to the injected |
| 219 | * script. |
| 220 | * |
| 221 | * <p>The injected script will execute in the context of PhantomJS's |
| 222 | * {@code page} variable. If a page has not been loaded before calling this |
| 223 | * method, one will be created.</p> |
| 224 | * |
| 225 | * <p>Be sure to wrap callback definitions in a try/catch block, as failures |
| 226 | * may cause future WebDriver calls to fail.</p> |
| 227 | * |
| 228 | * <p>Certain callbacks are used by GhostDriver (the PhantomJS WebDriver |
| 229 | * implementation) and overriding these may cause the script to fail. It is |
| 230 | * recommended that you check for existing callbacks before defining your own. |
| 231 | * </p> |
| 232 | * |
| 233 | * As with {@link #executeScript}, the injected script may be defined as |
| 234 | * a string for an anonymous function body (e.g. "return 123;"), or as a |
| 235 | * function. If a function is provided, it will be decompiled to its original |
| 236 | * source. Note that injecting functions is provided as a convenience to |
| 237 | * simplify defining complex scripts. Care must be taken that the function |
| 238 | * only references variables that will be defined in the page's scope and |
| 239 | * that the function does not override {@code Function.prototype.toString} |
| 240 | * (overriding toString() will interfere with how the function is |
| 241 | * decompiled. |
| 242 | * |
| 243 | * @param {(string|!Function)} script The script to execute. |
| 244 | * @param {...*} var_args The arguments to pass to the script. |
| 245 | * @return {!webdriver.promise.Promise<T>} A promise that resolve to the |
| 246 | * script's return value. |
| 247 | * @template T |
| 248 | */ |
| 249 | Driver.prototype.executePhantomJS = function(script, args) { |
| 250 | if (typeof script === 'function') { |
| 251 | script = 'return (' + script + ').apply(this, arguments);'; |
| 252 | } |
| 253 | var args = arguments.length > 1 |
| 254 | ? Array.prototype.slice.call(arguments, 1) : []; |
| 255 | return this.schedule( |
| 256 | new webdriver.Command(Command.EXECUTE_PHANTOM_SCRIPT) |
| 257 | .setParameter('script', script) |
| 258 | .setParameter('args', args), |
| 259 | 'Driver.executePhantomJS()'); |
| 260 | }; |
| 261 | |
| 262 | |
| 263 | // PUBLIC API |
| 264 | |
| 265 | exports.Driver = Driver; |