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
18var fs = require('fs'),
19 util = require('util');
20
21var 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 */
34var 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 */
43var 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 */
51var 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 */
59var DEFAULT_LOG_FILE = 'phantomjsdriver.log';
60
61
62/**
63 * Custom command names supported by PhantomJS.
64 * @enum {string}
65 */
66var 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 */
78function 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 */
100var 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 */
119function 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 */
129function 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 */
151var 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};
214util.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 */
222Driver.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 */
260Driver.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
276exports.Driver = Driver;
277exports.createDriver = createDriver;