Coverage

5%
152
8
144

/home/ubuntu/src/github.com/dalekjs/dalek-browser-chrome/index.js

5%
152
8
144
LineHitsSource
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
251'use strict';
26
27// ext. libs
281var Q = require('q');
291var fs = require('fs');
301var cp = require('child_process');
311var portscanner = require('portscanner');
32
33// int. libs
341var chromedriver = require('./lib/chromedriver');
35
36/**
37 * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek).
38 * It provides all a WebDriverServer & browser launcher for Google Chrome.
39 *
40 * The browser plugin can be installed with the following command:
41 *
42 * ```bash
43 * $ npm install dalek-browser-chrome --save-dev
44 * ```
45 *
46 * You can use the browser plugin by adding a config option to the your [Dalekfile](/pages/config.html)
47 *
48 * ```javascript
49 * "browser": ["chrome"]
50 * ```
51 *
52 * Or you can tell Dalek that it should test in this browser via the command line:
53 *
54 * ```bash
55 * $ dalek mytest.js -b chrome
56 * ```
57 *
58 * The Webdriver Server tries to open Port 9002 by default,
59 * if this port is blocked, it tries to use a port between 9003 & 9092
60 * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so:
61 *
62 * ```javascript
63 * "browsers": [{
64 * "chrome": {
65 * "port": 5555
66 * }
67 * }]
68 * ```
69 *
70 * It is also possible to specify a range of ports:
71 *
72 * ```javascript
73 * "browsers": [{
74 * "chrome": {
75 * "portRange": [6100, 6120]
76 * }
77 * }]
78 * ```
79 *
80 * If you would like to test Chrome Canary oder Chromium releases, you can simply apply a snd. argument,
81 * which defines the browser type:
82 *
83 * ```bash
84 * $ dalek mytest.js -b chrome:canary
85 * ```
86 *
87 * for canary, and if you would like to use chromium, just append `:chromium`:
88 *
89 * ```bash
90 * $ dalek mytest.js -b chrome:chromium
91 * ```
92 *
93 * This will only work if you installed your browser in the default locations,
94 * if the browsers binary is located in a non default location, you are able to specify
95 * its location in your [Dalekfile](/pages/config.html):
96 *
97 * ```javascript
98 * "browsers": [{
99 * "chrome": {
100 * "binary": "/Applications/Custom Located Chrome.app/MacOS/Contents/Chrome"
101 * }
102 * }]
103 * ```
104 *
105 * This also works for the canary & chromium builds
106 *
107 * ```javascript
108 * "browsers": [{
109 * "chrome": {
110 * "binary": "/Applications/Custom Located Chrome.app/MacOS/Contents/Chrome"
111 * }
112 * }]
113 * ```
114 *
115 * @module DalekJS
116 * @class ChromeDriver
117 * @namespace Browser
118 * @part Chrome
119 * @api
120 */
121
1221var ChromeDriver = {
123
124 /**
125 * Verbose version of the browser name
126 *
127 * @property longName
128 * @type string
129 * @default Google Chrome
130 */
131
132 longName: 'Google Chrome',
133
134 /**
135 * Default port of the ChromeWebDriverServer
136 * The port may change, cause the port conflict resolution
137 * tool might pick another one, if the default one is blocked
138 *
139 * @property port
140 * @type integer
141 * @default 9002
142 */
143
144 port: 9002,
145
146 /**
147 * Default maximum port of the ChromeWebDriverServer
148 * The port is the highest port in the range that can be allocated
149 * by the ChromeWebDriverServer
150 *
151 * @property maxPort
152 * @type integer
153 * @default 9092
154 */
155
156 maxPort: 9092,
157
158 /**
159 * Default host of the ChromeWebDriverServer
160 * The host may be overridden with
161 * a user configured value
162 *
163 * @property host
164 * @type string
165 * @default localhost
166 */
167
168 host: 'localhost',
169
170 /**
171 * Default desired capabilities that should be
172 * transferred when the browser session gets requested
173 *
174 * @property desiredCapabilities
175 * @type object
176 */
177
178 desiredCapabilities: {
179 browserName: 'chrome',
180 chromeOptions: {}
181 },
182
183 /**
184 * Driver defaults, what should the driver be able to access.
185 *
186 * @property driverDefaults
187 * @type object
188 */
189
190 driverDefaults: {
191 viewport: true,
192 status: true,
193 sessionInfo: true
194 },
195
196 /**
197 * Root path of the ChromeWebDriverServer
198 *
199 * @property path
200 * @type string
201 * @default /wd/hub
202 */
203
204 path: '/wd/hub',
205
206 /**
207 * Child process instance of the Chrome browser
208 *
209 * @property spawned
210 * @type null|Object
211 * @default null
212 */
213
214 spawned: null,
215
216 /**
217 * Chrome processes that are running on startup,
218 * and therefor shouldn`t be closed
219 *
220 * @property openProcesses
221 * @type array
222 * @default []
223 */
224
225 openProcesses: [],
226
227 /**
228 * Name of the process (platform dependent)
229 * that represents the browser itself
230 *
231 * @property processName
232 * @type string
233 * @default chrome.exe / Chrome
234 */
235
236 processName: (process.platform === 'win32' ? 'chrome.exe' : 'Chrome'),
237
238 /**
239 * Different browser types (Canary / Chromium) that can be controlled
240 * via the Chromedriver
241 *
242 * @property browserTypes
243 * @type object
244 */
245
246 browserTypes: {
247
248 /**
249 * Chrome Canary
250 *
251 * @property canary
252 * @type object
253 */
254
255 canary: {
256 name: 'Chrome Canary',
257 linux: 'google-chrome-canary',
258 darwin: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
259 win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe'
260 },
261
262 /**
263 * Chromium
264 *
265 * @property chromium
266 * @type object
267 */
268
269 chromium: {
270 name: 'Chromium',
271 process: (process.platform === 'win32' ? 'chromium.exe' : 'Chromium'),
272 linux: 'chromium-browser',
273 darwin: '/Applications/Chromium.app/Contents/MacOS/Chromium',
274 win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe'
275 }
276 },
277
278 /**
279 * Resolves the driver port
280 *
281 * @method getPort
282 * @return {integer} port WebDriver server port
283 */
284
285 getPort: function () {
2860 return this.port;
287 },
288
289 /**
290 * Resolves the maximum range for the driver port
291 *
292 * @method getMaxPort
293 * @return {integer} port Max WebDriver server port range
294 */
295
296 getMaxPort: function () {
2970 return this.maxPort;
298 },
299
300 /**
301 * Returns the driver host
302 *
303 * @method getHost
304 * @return {string} host WebDriver server hostname
305 */
306
307 getHost: function () {
3080 return this.host;
309 },
310
311 /**
312 * Launches the ChromeWebDriverServer
313 * (which implicitly launches Chrome itself)
314 * and checks for an available port
315 *
316 * @method launch
317 * @param {object} configuration Browser configuration
318 * @param {EventEmitter2} events EventEmitter (Reporter Emitter instance)
319 * @param {Dalek.Internal.Config} config Dalek configuration class
320 * @return {object} promise Browser promise
321 */
322
323 launch: function (configuration, events, config) {
3240 var deferred = Q.defer();
325
326 // store injected configuration/log event handlers
3270 this.reporterEvents = events;
3280 this.configuration = configuration;
3290 this.config = config;
330
331 // check for a user set port
3320 var browsers = this.config.get('browsers');
3330 if (browsers && Array.isArray(browsers)) {
3340 browsers.forEach(this._checkUserDefinedPorts.bind(this));
335 }
336
3370 if (configuration) {
3380 if (configuration.chromeOptions) {
3390 this.desiredCapabilities.chromeOptions = configuration.chromeOptions;
340 }
341
342 // check for a user defined binary
3430 if (configuration.binary) {
3440 var binaryExists = this._checkUserDefinedBinary(configuration.binary);
3450 if (binaryExists) {
346 // check for new verbose & process name
3470 this.longName = this._modifyVerboseBrowserName(configuration);
3480 this.processName = this._fetchProcessName(configuration);
349 }
350 }
351 }
352
353 // check if the current port is in use, if so, scan for free ports
3540 portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred));
3550 return deferred.promise;
356 },
357
358 /**
359 * Kills the ChromeWebDriverServer
360 * & Chrome browser processes
361 *
362 * @method kill
363 * @chainable
364 */
365
366 kill: function () {
3670 this._processes(process.platform, this._checkProcesses.bind(this));
3680 return this;
369 },
370
371 /**
372 * Modifies the verbose browser name
373 *
374 * @method _modifyVerboseBrowserName
375 * @param {object} configuration User configuration
376 * @return {string} Verbose browser name
377 * @private
378 */
379
380 _modifyVerboseBrowserName: function (configuration) {
3810 if (configuration.type && this.browserTypes[configuration.type]) {
3820 return this.browserTypes[configuration.type].name + ' (' + this.longName + ')';
383 }
384
3850 return this.longName;
386 },
387
388 /**
389 * Change the process name for browser instances like Canary & Chromium
390 *
391 * @method _fetchProcessName
392 * @param {object} configuration User configuration
393 * @return {string} Verbose browser name
394 * @private
395 */
396
397 _fetchProcessName: function (configuration) {
398 // check if the process name must be overridden (to shut down the browser)
3990 if (this.browserTypes[configuration.type] && this.browserTypes[configuration.type].process) {
4000 return this.browserTypes[configuration.type].process;
401 }
402
4030 return this.processName;
404 },
405
406 /**
407 * Process user defined ports
408 *
409 * @method _checkUserDefinedPorts
410 * @param {object} browser Browser configuration
411 * @chainable
412 * @private
413 */
414
415 _checkUserDefinedPorts: function (browser) {
416 // check for a single defined port
4170 if (browser.chrome && browser.chrome.port) {
4180 this.port = parseInt(browser.chrome.port, 10);
4190 this.maxPort = this.port + 90;
4200 this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to user defined port: ' + this.port);
421 }
422
423 // check for a port range
4240 if (browser.chrome && browser.chrome.portRange && browser.chrome.portRange.length === 2) {
4250 this.port = parseInt(browser.chrome.portRange[0], 10);
4260 this.maxPort = parseInt(browser.chrome.portRange[1], 10);
4270 this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort);
428 }
429
4300 return this;
431 },
432
433 /**
434 * Checks if the binary exists,
435 * when set manually by the user
436 *
437 * @method _checkUserDefinedBinary
438 * @param {string} binary Path to the browser binary
439 * @return {bool} Binary exists
440 * @private
441 */
442
443 _checkUserDefinedBinary: function (binary) {
444 // check if we need to replace the users home directory
4450 if (process.platform === 'darwin' && binary.trim()[0] === '~') {
4460 binary = binary.replace('~', process.env.HOME);
447 }
448
449 // check if the binary exists
4500 if (!fs.existsSync(binary)) {
4510 this.reporterEvents.emit('error', 'dalek-driver-chrome: Binary not found: ' + binary);
4520 process.exit(127);
4530 return false;
454 }
455
456 // add the binary to the desired capabilities
4570 this.desiredCapabilities.chromeOptions.binary = binary;
458
4590 return true;
460 },
461
462 /**
463 * Checks if the def. port is blocked & if we need to switch to another port
464 * Kicks off the process manager (for closing the opened browsers after the run has been finished)
465 * Also starts the chromedriver instance
466 *
467 * @method _checkPorts
468 * @param {object} deferred Promise
469 * @param {null|object} error Error object
470 * @param {integer} port Found open port
471 * @private
472 * @chainable
473 */
474
475 _checkPorts: function(deferred, error, port) {
476 // check if the port was blocked & if we need to switch to another port
4770 if (this.port !== port) {
4780 this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to port: ' + port);
4790 this.port = port;
480 }
481
482 // get the currently running processes & invoke the chromedriver afterwards
4830 this._processes(process.platform, this._startChromedriver.bind(this, deferred));
4840 return this;
485 },
486
487 /**
488 * Spawns an instance of Chromedriver
489 *
490 * @method _startChromedriver
491 * @param {object} deferred Promise
492 * @param {null|object} error Error object
493 * @param {string} result List of open chrome processes BEFORE the test browser has been launched
494 * @private
495 * @chainable
496 */
497
498 _startChromedriver: function (deferred, err, result) {
4990 var args = ['--port=' + this.getPort(), '--url-base=' + this.path];
5000 this.spawned = cp.spawn(chromedriver.path, args);
5010 this.openProcesses = result;
5020 this.spawned.stdout.on('data', this._catchDriverLogs.bind(this, deferred));
5030 return this;
504 },
505
506 /**
507 * Watches the chromedriver console output to capture the starting message
508 *
509 * @method _catchDriverLogs
510 * @param {object} deferred Promise
511 * @param {buffer} data Chromedriver console output
512 * @private
513 * @chainable
514 */
515
516 _catchDriverLogs: function (deferred, data) {
5170 var dataStr = data + '';
5180 var timeout = null;
519
520 // timeout to catch if chromedriver couldnt be launched
5210 if (dataStr.search('DVFreeThread') === -1) {
5220 timeout = setTimeout(function () {
5230 deferred.reject();
5240 this.reporterEvents.emit('error', 'Chromedriver: ' + dataStr.trim());
5250 this.reporterEvents.emit('error', 'dalek-driver-chrome: Could not launch Chromedriver');
5260 process.exit(127);
527 }.bind(this), 2000);
528 }
529
530 // look for the success message
5310 if (dataStr.search('Starting ChromeDriver') !== -1) {
5320 this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Started ChromeDriver');
5330 if (timeout !== null) {
5340 clearTimeout(timeout);
535 }
5360 deferred.resolve();
537 }
538
5390 return this;
540 },
541
542 /**
543 * Remove the chromedriver log that is written to the current working directory
544 *
545 * @method _unlinkChromedriverLog
546 * @param {bool} retry Delete has been tried min 1 time before
547 * @private
548 * @chainable
549 */
550
551 _unlinkChromedriverLog: function (retry) {
5520 var logfile = process.cwd() + '/chromedriver.log';
5530 try {
5540 if (fs.existsSync(logfile)) {
5550 fs.unlinkSync(logfile);
556 }
557 } catch (e) {
5580 if (!retry) {
5590 setTimeout(this._unlinkChromedriverLog.bind(this, true), 1000);
560 }
561 }
562
5630 return this;
564 },
565
566 /**
567 * Tracks running browser processes for chrome on mac & linux
568 *
569 * @method _processes
570 * @param {string} platform Current OS
571 * @param {function} fn Callback
572 * @chainable
573 * @private
574 */
575
576 _processes: function (platform, fn) {
5770 if (platform === 'win32') {
5780 this._processesWin(fn);
5790 return this;
580 }
581
5820 this._processesNix(fn);
5830 return this;
584 },
585
586 /**
587 * Kills all associated processes
588 *
589 * @method _checkProcesses
590 * @param {object|null} err Error object or null
591 * @param {array} result List of running processes
592 * @chainable
593 * @private
594 */
595
596 _checkProcesses: function (err, result) {
597 // log that the driver shutdown process has been initiated
5980 this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Shutting down ChromeDriver');
599 // kill leftover chrome browser processes
6000 result.forEach(this[(process.platform === 'win32' ? '_killWindows' : '_killNix')].bind(this));
601 // kill chromedriver binary
6020 this.spawned.kill('SIGTERM');
603 // clean up the file mess the chromedriver leaves us behind
6040 this._unlinkChromedriverLog();
6050 return this;
606 },
607
608 // UNIX ONLY
609 // ---------
610
611 /**
612 * Kills a process
613 *
614 * @method _killNix
615 * @param {integer} processID Process ID
616 * @chainable
617 * @private
618 */
619
620 _killNix: function (processID) {
6210 var kill = true;
6220 this.openProcesses.forEach(function (pid) {
6230 if (pid === processID) {
6240 kill = false;
625 }
626 });
627
6280 if (kill === true) {
6290 var killer = cp.spawn;
6300 killer('kill', [processID]);
631 }
632
6330 return this;
634 },
635
636 /**
637 * Lists all chrome processes on *NIX systems
638 *
639 * @method _processesNix
640 * @param {function} fn Calback
641 * @chainable
642 * @private
643 */
644
645 _processesNix: function (fn) {
6460 var processName = process.platform === 'darwin' ? this.processName : this.processName.toLowerCase();
6470 var cmd = ['ps -ax', '|', 'grep ' + processName];
6480 cp.exec(cmd.join(' '), this._processListNix.bind(this, fn));
6490 return this;
650 },
651
652 /**
653 * Deserializes a process list on nix
654 *
655 * @method _processListNix
656 * @param {function} fn Calback
657 * @param {object|null} err Error object
658 * @param {string} stdout Output of the process list shell command
659 * @chainable
660 * @private
661 */
662
663 _processListNix: function(fn, err, stdout) {
6640 var result = [];
6650 stdout.split('\n').forEach(this._splitProcessListNix.bind(this, result));
6660 fn(err, result);
6670 return this;
668 },
669
670 /**
671 * Reformats the process list output on *NIX systems
672 *
673 * @method _splitProcessListNix
674 * @param {array} result Reference to the process list
675 * @param {string} line Single process in text representation
676 * @chainable
677 * @private
678 */
679
680 _splitProcessListNix: function(result, line) {
6810 var data = line.split(' ');
6820 data = data.filter(this._filterProcessItemsNix.bind(this));
6830 result.push(data[0]);
6840 return this;
685 },
686
687 /**
688 * Filters empty process list entries on *NIX
689 *
690 * @method _filterProcessItemsNix
691 * @param {string} item Item to check
692 * @return {string|bool} Item or falsy
693 * @private
694 */
695
696 _filterProcessItemsNix: function (item) {
6970 if (item !== '') {
6980 return item;
699 }
7000 return false;
701 },
702
703 // WIN ONLY
704 // --------
705
706 /**
707 * Lists all running processes (win only)
708 *
709 * @method _processesWin
710 * @param {Function} callback Receives the process object as the only callback argument
711 * @chainable
712 * @private
713 */
714
715 _processesWin: function (callback) {
7160 cp.exec('tasklist /FO CSV', this._processListWin.bind(this, callback));
7170 return this;
718 },
719
720 /**
721 * Deserializes the process list on win
722 *
723 * @method _processListWin
724 * @param {function} callback Callback to be executed after the list has been transformed
725 * @param {object|null} err Error if error, else null
726 * @param {string} stdout Output of the process list command
727 * @chainable
728 * @private
729 */
730
731 _processListWin: function (callback, err, stdout) {
7320 var p = [];
7330 stdout.split('\r\n').forEach(this._splitProcessListWin.bind(this, p));
7340 var proc = [];
7350 var head = null;
7360 while (p.length > 1) {
7370 var rec = p.shift();
7380 rec = rec.replace(/\"\,/gi,'";').replace(/\"|\'/gi,'').split(';');
7390 if (head === null){
7400 head = rec;
7410 for (var i=0;i<head.length;i++){
7420 head[i] = head[i].replace(/ /gi,'');
743 }
744 } else {
7450 var tmp = {};
7460 for (var j=0;j<rec.length;j++){
7470 tmp[head[j]] = rec[j].replace(/\"|\'/gi,'');
748 }
7490 proc.push(tmp);
750 }
751 }
752
7530 var result = [];
7540 proc.forEach(this._filterProcessItemsWin.bind(this, result));
7550 callback(null, result);
7560 return this;
757 },
758
759 /**
760 * Processes (transforms) the list of processes
761 *
762 * @method _filterProcessItemsWin
763 * @param {array} result Reference to the result process list
764 * @param {object} process Single piece of process information
765 * @chainable
766 * @private
767 */
768
769 _filterProcessItemsWin: function (result, process) {
7700 Object.keys(process).forEach(function (key) {
7710 if (process[key] === this.processName) {
7720 result.push(process.PID);
773 }
774 }.bind(this));
7750 return this;
776 },
777
778 /**
779 * Filters empty lines out of the process result
780 *
781 * @method _splitProcessListWin
782 * @param {array} p Reference to the process list
783 * @param {string} line Process item
784 * @chainable
785 * @private
786 */
787
788 _splitProcessListWin: function (p, line) {
7890 if (line.trim().length !== 0) {
7900 p.push(line);
791 }
7920 return this;
793 },
794
795 /**
796 * Kills a process (based on a PID)
797 * that was not opened BEFORE Dalek has
798 * been started
799 *
800 * @method _killWindows
801 * @param {integer} pid Process id
802 * @chainable
803 * @private
804 */
805
806 _killWindows: function (pid) {
8070 var kill = true;
808
809 // check if the process was running BEFORE we started dalek
8100 this.openProcesses.forEach(function (opid) {
8110 if (opid === pid) {
8120 kill = false;
813 }
814 });
815
816 // kill the browser process
8170 if (kill === true) {
8180 cp.exec('taskkill /PID ' + pid + ' /f', function(){});
819 }
820
8210 return this;
822 }
823
824};
825
826// expose the module
8271module.exports = ChromeDriver;
828