1 | | /*! |
2 | | * |
3 | | * Copyright (c) 2013 Sebastian Golasch |
4 | | * |
5 | | * Permission is hereby granted, free of charge, to any person obtaining a |
6 | | * copy of this software and associated documentation files (the "Software"), |
7 | | * to deal in the Software without restriction, including without limitation |
8 | | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
9 | | * and/or sell copies of the Software, and to permit persons to whom the |
10 | | * Software is furnished to do so, subject to the following conditions: |
11 | | * |
12 | | * The above copyright notice and this permission notice shall be included |
13 | | * in all copies or substantial portions of the Software. |
14 | | * |
15 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
16 | | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
17 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
18 | | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
19 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
20 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
21 | | * DEALINGS IN THE SOFTWARE. |
22 | | * |
23 | | */ |
24 | | |
25 | 1 | 'use strict'; |
26 | | |
27 | | // ext. libs |
28 | 1 | var Q = require('q'); |
29 | 1 | var fs = require('fs'); |
30 | 1 | var phantomjs = require('phantomjs'); |
31 | 1 | var portscanner = require('portscanner'); |
32 | 1 | var spawn = require('child_process').spawn; |
33 | | |
34 | | /** |
35 | | * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek). |
36 | | * It provides a browser launcher as well the PhantomJS browser itself. |
37 | | * |
38 | | * The browser plugin comes bundled with the DalekJS base framework. |
39 | | * |
40 | | * You can use the browser plugin beside others (it is the default) |
41 | | * by adding a config option to the your Dalekfile: |
42 | | * |
43 | | * ```javascript |
44 | | * "browser": ["phantomjs", "chrome"] |
45 | | * ``` |
46 | | * |
47 | | * Or you can tell Dalek that it should test in this & another browser via the command line: |
48 | | * |
49 | | * ```bash |
50 | | * $ dalek mytest.js -b phantomjs,chrome |
51 | | * ``` |
52 | | * |
53 | | * The Webdriver Server tries to open Port 9001 by default, |
54 | | * if this port is blocked, it tries to use a port between 9002 & 9091 |
55 | | * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so: |
56 | | * |
57 | | * ```javascript |
58 | | * "browsers": { |
59 | | * "phantomjs": { |
60 | | * "port": 5555 |
61 | | * } |
62 | | * } |
63 | | * ``` |
64 | | * |
65 | | * It is also possible to specify a range of ports: |
66 | | * |
67 | | * ```javascript |
68 | | * "browsers": { |
69 | | * "phantomjs": { |
70 | | * "portRange": [6100, 6120] |
71 | | * } |
72 | | * } |
73 | | * ``` |
74 | | * |
75 | | * If you would like to use a different Phantom version than the one that comes bundled with the driver, |
76 | | * your are able to specify its location in your [Dalekfile](/pages/config.html): |
77 | | * |
78 | | * ```javascript |
79 | | * "browsers": { |
80 | | * "phantomjs": { |
81 | | * "binary": "~/bin/phantomjs" |
82 | | * } |
83 | | * } |
84 | | * ``` |
85 | | * |
86 | | * If you would like to preserve the ability to use the bundled version, |
87 | | * you can also add an additional browser launcher in your [Dalekfile](/pages/config.html). |
88 | | * |
89 | | * ```javascript |
90 | | * "browsers": { |
91 | | * "phantomjs:1.9.1": { |
92 | | * "binary": "~/bin/phantomjs" |
93 | | * } |
94 | | * } |
95 | | * ``` |
96 | | * |
97 | | * And then launch it like this: |
98 | | * |
99 | | * ```bash |
100 | | * $ dalek mytest.js -b phantomjs:1.9.1 |
101 | | * ``` |
102 | | * |
103 | | * @module DalekJS |
104 | | * @class PhantomJSDriver |
105 | | * @namespace Browser |
106 | | * @part PhantomJS |
107 | | * @api |
108 | | */ |
109 | | |
110 | 1 | var PhantomJSDriver = { |
111 | | |
112 | | /** |
113 | | * Verbose version of the browser name |
114 | | * |
115 | | * @property |
116 | | * @type string |
117 | | * @default PhantomJS |
118 | | */ |
119 | | |
120 | | longName: 'PhantomJS', |
121 | | |
122 | | /** |
123 | | * Default port of the PhantomJSDriver |
124 | | * The port may change, cause the port conflict resultion |
125 | | * tool might pick another one, if the default one is blocked |
126 | | * |
127 | | * @property |
128 | | * @type integer |
129 | | * @default 9001 |
130 | | */ |
131 | | |
132 | | port: 9001, |
133 | | |
134 | | /** |
135 | | * Default maximum port of the Ghostdriver Server |
136 | | * The port is the highest port in the range that can be allocated |
137 | | * by the Ghostdriver Server |
138 | | * |
139 | | * @property maxPort |
140 | | * @type integer |
141 | | * @default 9091 |
142 | | */ |
143 | | |
144 | | maxPort: 9091, |
145 | | |
146 | | /** |
147 | | * Default host of the PhantomJSDriver |
148 | | * The host may be overriden with |
149 | | * a user configured value |
150 | | * |
151 | | * @property |
152 | | * @type string |
153 | | * @default localhost |
154 | | */ |
155 | | |
156 | | host: 'localhost', |
157 | | |
158 | | /** |
159 | | * Root path of the PhantomJSDriver |
160 | | * |
161 | | * @property |
162 | | * @type string |
163 | | * @default /wd/hub |
164 | | */ |
165 | | |
166 | | path: '/wd/hub', |
167 | | |
168 | | /** |
169 | | * Default desired capabilities that should be |
170 | | * transferred when the browser session gets requested |
171 | | * |
172 | | * @property desiredCapabilities |
173 | | * @type object |
174 | | */ |
175 | | |
176 | | desiredCapabilities: { |
177 | | version: phantomjs.version, |
178 | | browserName: 'phantomjs' |
179 | | }, |
180 | | |
181 | | /** |
182 | | * Driver defaults, what should the driver be able to access. |
183 | | * |
184 | | * @property driverDefaults |
185 | | * @type object |
186 | | */ |
187 | | |
188 | | driverDefaults: { |
189 | | viewport: true, |
190 | | status: true, |
191 | | sessionInfo: true |
192 | | }, |
193 | | |
194 | | /** |
195 | | * Child process instance of the PhantomJS browser |
196 | | * |
197 | | * @property |
198 | | * @type null|Object |
199 | | */ |
200 | | |
201 | | spawned: null, |
202 | | |
203 | | /** |
204 | | * Resolves the driver port |
205 | | * |
206 | | * @method getPort |
207 | | * @return integer |
208 | | */ |
209 | | |
210 | | getPort: function () { |
211 | 0 | return this.port; |
212 | | }, |
213 | | |
214 | | /** |
215 | | * Resolves the maximum range for the driver port |
216 | | * |
217 | | * @method getMaxPort |
218 | | * @return {integer} port Max WebDriver server port range |
219 | | */ |
220 | | |
221 | | getMaxPort: function () { |
222 | 0 | return this.maxPort; |
223 | | }, |
224 | | |
225 | | /** |
226 | | * Returns the driver host |
227 | | * |
228 | | * @method getHost |
229 | | * @type string |
230 | | */ |
231 | | |
232 | | getHost: function () { |
233 | 0 | return this.host; |
234 | | }, |
235 | | |
236 | | /** |
237 | | * Launches PhantomJS, negoatiates a port & checks for a user set binary |
238 | | * |
239 | | * @method launch |
240 | | * @param {object} configuration Browser configuration |
241 | | * @param {EventEmitter2} events EventEmitter (Reporter Emitter instance) |
242 | | * @param {Dalek.Internal.Config} config Dalek configuration class |
243 | | * @return {object} promise Browser promise |
244 | | */ |
245 | | |
246 | | launch: function (configuration, events, config) { |
247 | 0 | var deferred = Q.defer(); |
248 | | |
249 | | // store injected configuration/log event handlers |
250 | 0 | this.reporterEvents = events; |
251 | 0 | this.configuration = configuration; |
252 | 0 | this.config = config; |
253 | | |
254 | | // check for a user set port |
255 | 0 | var browsers = this.config.get('browsers'); |
256 | 0 | if (browsers && Array.isArray(browsers)) { |
257 | 0 | browsers.forEach(this._checkUserDefinedPorts.bind(this)); |
258 | | } |
259 | | |
260 | | // check if the current port is in use, if so, scan for free ports |
261 | 0 | portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred)); |
262 | 0 | return deferred.promise; |
263 | | }, |
264 | | |
265 | | /** |
266 | | * Kills the PhantomJSDriver processe |
267 | | * |
268 | | * @method kill |
269 | | * @chainable |
270 | | */ |
271 | | |
272 | | kill: function () { |
273 | 0 | this.spawned.kill('SIGTERM'); |
274 | 0 | return this; |
275 | | }, |
276 | | |
277 | | /** |
278 | | * Checks if the def. port is blocked & if we need to switch to another port |
279 | | * Kicks off the process manager (for closing the opened browsers after the run has been finished) |
280 | | * Also starts the chromedriver instance |
281 | | * |
282 | | * @method _checkPorts |
283 | | * @param {object} deferred Promise |
284 | | * @param {null|object} error Error object |
285 | | * @param {integer} port Found open port |
286 | | * @private |
287 | | * @chainable |
288 | | */ |
289 | | |
290 | | _checkPorts: function (deferred, error, port) { |
291 | | // check if the port was blocked & if we need to switch to another port |
292 | 0 | if (this.port !== port) { |
293 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-phantomjs: Switching to port: ' + port); |
294 | 0 | this.port = port; |
295 | | } |
296 | | |
297 | | // check the binary |
298 | 0 | var binary = this._checkUserDefinedBinary(this.configuration, phantomjs.path); |
299 | | |
300 | | // launch the browser process |
301 | 0 | this.spawned = spawn(binary, ['--webdriver', this.getPort(), '--ignore-ssl-errors=true']); |
302 | 0 | this.spawned.stdout.on('data', this._launch.bind(this, deferred)); |
303 | 0 | return this; |
304 | | }, |
305 | | |
306 | | /** |
307 | | * Checks the data stream from the launched phantom process |
308 | | * |
309 | | * @method _launch |
310 | | * @param {object} deferred Promise |
311 | | * @param {buffer} data Console output from Ghostdriver |
312 | | * @chainable |
313 | | * @private |
314 | | */ |
315 | | |
316 | | _launch: function (deferred, data) { |
317 | 0 | var stream = data + ''; |
318 | | |
319 | | // check if ghostdriver could be launched |
320 | 0 | if (stream.search('GhostDriver - Main - running') !== -1) { |
321 | 0 | deferred.resolve(); |
322 | 0 | } else if (stream.search('Could not start Ghost Driver') !== -1) { |
323 | 0 | this.reporterEvents.emit('error', 'dalek-browser-phantomjs: Could not start Ghost Driver'); |
324 | 0 | deferred.reject('Could not start Ghost Driver'); |
325 | 0 | process.exit(127); |
326 | | } |
327 | | |
328 | 0 | return this; |
329 | | }, |
330 | | |
331 | | /** |
332 | | * Process user defined ports |
333 | | * |
334 | | * @method _checkUserDefinedPorts |
335 | | * @param {object} browser Browser configuration |
336 | | * @chainable |
337 | | * @private |
338 | | */ |
339 | | |
340 | | _checkUserDefinedPorts: function (browser) { |
341 | | // check for a single defined port |
342 | 0 | if (browser.phantomjs && browser.phantomjs.port) { |
343 | 0 | this.port = parseInt(browser.phantomjs.port, 10); |
344 | 0 | this.maxPort = this.port + 90; |
345 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-phantomjs: Switching to user defined port: ' + this.port); |
346 | | } |
347 | | |
348 | | // check for a port range |
349 | 0 | if (browser.phantomjs && browser.phantomjs.portRange && browser.phantomjs.portRange.length === 2) { |
350 | 0 | this.port = parseInt(browser.phantomjs.portRange[0], 10); |
351 | 0 | this.maxPort = parseInt(browser.phantomjs.portRange[1], 10); |
352 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-phantomjs: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort); |
353 | | } |
354 | | |
355 | 0 | return this; |
356 | | }, |
357 | | |
358 | | /** |
359 | | * Checks if the binary exists, |
360 | | * when set manually by the user |
361 | | * |
362 | | * @method _checkUserDefinedBinary |
363 | | * @param {string} binary Path to the browser binary |
364 | | * @return {bool|string} Binary path if binary exists, else false |
365 | | * @private |
366 | | */ |
367 | | |
368 | | _checkUserDefinedBinary: function (configuration, defaultBinary) { |
369 | 0 | var binary = defaultBinary; |
370 | | |
371 | | // check if we have a user defined binary |
372 | 0 | if (configuration && configuration.binary) { |
373 | 0 | binary = configuration.binary; |
374 | | } |
375 | | |
376 | | // check if we need to replace the users home directory |
377 | 0 | if (process.platform === 'darwin' && binary.trim()[0] === '~') { |
378 | 0 | binary = binary.replace('~', process.env.HOME); |
379 | | } |
380 | | |
381 | | // check if the binary exists |
382 | 0 | if (!fs.existsSync(binary)) { |
383 | 0 | this.reporterEvents.emit('error', 'dalek-driver-phantomjs: Binary not found: ' + binary); |
384 | 0 | process.exit(127); |
385 | 0 | return false; |
386 | | } |
387 | | |
388 | 0 | return binary; |
389 | | } |
390 | | |
391 | | }; |
392 | | |
393 | 1 | module.exports = PhantomJSDriver; |
394 | | |