# 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 |
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;