phantomjs.js

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
20var fs = require('fs'),
21 util = require('util');
22
23var 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 */
36var 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 */
45var 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 */
53var 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 */
61var DEFAULT_LOG_FILE = 'phantomjsdriver.log';
62
63
64/**
65 * Custom command names supported by PhantomJS.
66 * @enum {string}
67 */
68var 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 */
80function 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 */
102var 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 */
118function 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 */
140var 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};
203util.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 */
211Driver.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 */
249Driver.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
265exports.Driver = Driver;