# phantomjs.js

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