Plato on Github
Report Home
index.js
Maintainability
70.03
Lines of code
803
Difficulty
42.42
Estimated Errors
3.58
Function weight
By Complexity
By SLOC
/*! * * Copyright (c) 2013 Sebastian Golasch * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ 'use strict'; // ext. libs var Q = require('q'); var fs = require('fs'); var cp = require('child_process'); var portscanner = require('portscanner'); // int. libs var chromedriver = require('./lib/chromedriver'); /** * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek). * It provides all a WebDriverServer & browser launcher for Google Chrome. * * The browser plugin can be installed with the following command: * * ```bash * $ npm install dalek-browser-chrome --save-dev * ``` * * You can use the browser plugin by adding a config option to the your [Dalekfile](/pages/config.html) * * ```javascript * "browser": ["chrome"] * ``` * * Or you can tell Dalek that it should test in this browser via the command line: * * ```bash * $ dalek mytest.js -b chrome * ``` * * The Webdriver Server tries to open Port 9002 by default, * if this port is blocked, it tries to use a port between 9003 & 9092 * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so: * * ```javascript * "browsers": [{ * "chrome": { * "port": 5555 * } * }] * ``` * * It is also possible to specify a range of ports: * * ```javascript * "browsers": [{ * "chrome": { * "portRange": [6100, 6120] * } * }] * ``` * * If you would like to test Chrome Canary oder Chromium releases, you can simply apply a snd. argument, * which defines the browser type: * * ```bash * $ dalek mytest.js -b chrome:canary * ``` * * for canary, and if you would like to use chromium, just append `:chromium`: * * ```bash * $ dalek mytest.js -b chrome:chromium * ``` * * This will only work if you installed your browser in the default locations, * if the browsers binary is located in a non default location, you are able to specify * its location in your [Dalekfile](/pages/config.html): * * ```javascript * "browsers": [{ * "chrome": { * "binary": "/Applications/Custom Located Chrome.app/MacOS/Contents/Chrome" * } * }] * ``` * * This also works for the canary & chromium builds * * ```javascript * "browsers": [{ * "chrome": { * "binary": "/Applications/Custom Located Chrome.app/MacOS/Contents/Chrome" * } * }] * ``` * * @module DalekJS * @class ChromeDriver * @namespace Browser * @part Chrome * @api */ var ChromeDriver = { /** * Verbose version of the browser name * * @property longName * @type string * @default Google Chrome */ longName: 'Google Chrome', /** * Default port of the ChromeWebDriverServer * The port may change, cause the port conflict resolution * tool might pick another one, if the default one is blocked * * @property port * @type integer * @default 9002 */ port: 9002, /** * Default maximum port of the ChromeWebDriverServer * The port is the highest port in the range that can be allocated * by the ChromeWebDriverServer * * @property maxPort * @type integer * @default 9092 */ maxPort: 9092, /** * Default host of the ChromeWebDriverServer * The host may be overridden with * a user configured value * * @property host * @type string * @default localhost */ host: 'localhost', /** * Default desired capabilities that should be * transferred when the browser session gets requested * * @property desiredCapabilities * @type object */ desiredCapabilities: { browserName: 'chrome', chromeOptions: {} }, /** * Driver defaults, what should the driver be able to access. * * @property driverDefaults * @type object */ driverDefaults: { viewport: true, status: true, sessionInfo: true }, /** * Root path of the ChromeWebDriverServer * * @property path * @type string * @default /wd/hub */ path: '/wd/hub', /** * Child process instance of the Chrome browser * * @property spawned * @type null|Object * @default null */ spawned: null, /** * Chrome processes that are running on startup, * and therefor shouldn`t be closed * * @property openProcesses * @type array * @default [] */ openProcesses: [], /** * Name of the process (platform dependent) * that represents the browser itself * * @property processName * @type string * @default chrome.exe / Chrome */ processName: (process.platform === 'win32' ? 'chrome.exe' : 'Chrome'), /** * Different browser types (Canary / Chromium) that can be controlled * via the Chromedriver * * @property browserTypes * @type object */ browserTypes: { /** * Chrome Canary * * @property canary * @type object */ canary: { name: 'Chrome Canary', linux: 'google-chrome-canary', darwin: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe' }, /** * Chromium * * @property chromium * @type object */ chromium: { name: 'Chromium', process: (process.platform === 'win32' ? 'chromium.exe' : 'Chromium'), linux: 'chromium-browser', darwin: '/Applications/Chromium.app/Contents/MacOS/Chromium', win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe' } }, /** * Resolves the driver port * * @method getPort * @return {integer} port WebDriver server port */ getPort: function () { return this.port; }, /** * Resolves the maximum range for the driver port * * @method getMaxPort * @return {integer} port Max WebDriver server port range */ getMaxPort: function () { return this.maxPort; }, /** * Returns the driver host * * @method getHost * @return {string} host WebDriver server hostname */ getHost: function () { return this.host; }, /** * Launches the ChromeWebDriverServer * (which implicitly launches Chrome itself) * and checks for an available port * * @method launch * @param {object} configuration Browser configuration * @param {EventEmitter2} events EventEmitter (Reporter Emitter instance) * @param {Dalek.Internal.Config} config Dalek configuration class * @return {object} promise Browser promise */ launch: function (configuration, events, config) { var deferred = Q.defer(); // store injected configuration/log event handlers this.reporterEvents = events; this.configuration = configuration; this.config = config; // check for a user set port var browsers = this.config.get('browsers'); if (browsers && Array.isArray(browsers)) { browsers.forEach(this._checkUserDefinedPorts.bind(this)); } if (configuration) { if (configuration.chromeOptions) { this.desiredCapabilities.chromeOptions = configuration.chromeOptions; } // check for a user defined binary if (configuration.binary) { var binaryExists = this._checkUserDefinedBinary(configuration.binary); if (binaryExists) { // check for new verbose & process name this.longName = this._modifyVerboseBrowserName(configuration); this.processName = this._fetchProcessName(configuration); } } } // check if the current port is in use, if so, scan for free ports portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred)); return deferred.promise; }, /** * Kills the ChromeWebDriverServer * & Chrome browser processes * * @method kill * @chainable */ kill: function () { this._processes(process.platform, this._checkProcesses.bind(this)); return this; }, /** * Modifies the verbose browser name * * @method _modifyVerboseBrowserName * @param {object} configuration User configuration * @return {string} Verbose browser name * @private */ _modifyVerboseBrowserName: function (configuration) { if (configuration.type && this.browserTypes[configuration.type]) { return this.browserTypes[configuration.type].name + ' (' + this.longName + ')'; } return this.longName; }, /** * Change the process name for browser instances like Canary & Chromium * * @method _fetchProcessName * @param {object} configuration User configuration * @return {string} Verbose browser name * @private */ _fetchProcessName: function (configuration) { // check if the process name must be overridden (to shut down the browser) if (this.browserTypes[configuration.type] && this.browserTypes[configuration.type].process) { return this.browserTypes[configuration.type].process; } return this.processName; }, /** * Process user defined ports * * @method _checkUserDefinedPorts * @param {object} browser Browser configuration * @chainable * @private */ _checkUserDefinedPorts: function (browser) { // check for a single defined port if (browser.chrome && browser.chrome.port) { this.port = parseInt(browser.chrome.port, 10); this.maxPort = this.port + 90; this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to user defined port: ' + this.port); } // check for a port range if (browser.chrome && browser.chrome.portRange && browser.chrome.portRange.length === 2) { this.port = parseInt(browser.chrome.portRange[0], 10); this.maxPort = parseInt(browser.chrome.portRange[1], 10); this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort); } return this; }, /** * Checks if the binary exists, * when set manually by the user * * @method _checkUserDefinedBinary * @param {string} binary Path to the browser binary * @return {bool} Binary exists * @private */ _checkUserDefinedBinary: function (binary) { // check if we need to replace the users home directory if (process.platform === 'darwin' && binary.trim()[0] === '~') { binary = binary.replace('~', process.env.HOME); } // check if the binary exists if (!fs.existsSync(binary)) { this.reporterEvents.emit('error', 'dalek-driver-chrome: Binary not found: ' + binary); process.exit(127); return false; } // add the binary to the desired capabilities this.desiredCapabilities.chromeOptions.binary = binary; return true; }, /** * Checks if the def. port is blocked & if we need to switch to another port * Kicks off the process manager (for closing the opened browsers after the run has been finished) * Also starts the chromedriver instance * * @method _checkPorts * @param {object} deferred Promise * @param {null|object} error Error object * @param {integer} port Found open port * @private * @chainable */ _checkPorts: function(deferred, error, port) { // check if the port was blocked & if we need to switch to another port if (this.port !== port) { this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Switching to port: ' + port); this.port = port; } // get the currently running processes & invoke the chromedriver afterwards this._processes(process.platform, this._startChromedriver.bind(this, deferred)); return this; }, /** * Spawns an instance of Chromedriver * * @method _startChromedriver * @param {object} deferred Promise * @param {null|object} error Error object * @param {string} result List of open chrome processes BEFORE the test browser has been launched * @private * @chainable */ _startChromedriver: function (deferred, err, result) { var args = ['--port=' + this.getPort(), '--url-base=' + this.path]; this.spawned = cp.spawn(chromedriver.path, args); this.openProcesses = result; this.spawned.stdout.on('data', this._catchDriverLogs.bind(this, deferred)); return this; }, /** * Watches the chromedriver console output to capture the starting message * * @method _catchDriverLogs * @param {object} deferred Promise * @param {buffer} data Chromedriver console output * @private * @chainable */ _catchDriverLogs: function (deferred, data) { var dataStr = data + ''; var timeout = null; // timeout to catch if chromedriver couldnt be launched if (dataStr.search('DVFreeThread') === -1) { timeout = setTimeout(function () { deferred.reject(); this.reporterEvents.emit('error', 'Chromedriver: ' + dataStr.trim()); this.reporterEvents.emit('error', 'dalek-driver-chrome: Could not launch Chromedriver'); process.exit(127); }.bind(this), 2000); } // look for the success message if (dataStr.search('Starting ChromeDriver') !== -1) { this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Started ChromeDriver'); if (timeout !== null) { clearTimeout(timeout); } deferred.resolve(); } return this; }, /** * Remove the chromedriver log that is written to the current working directory * * @method _unlinkChromedriverLog * @param {bool} retry Delete has been tried min 1 time before * @private * @chainable */ _unlinkChromedriverLog: function (retry) { var logfile = process.cwd() + '/chromedriver.log'; try { if (fs.existsSync(logfile)) { fs.unlinkSync(logfile); } } catch (e) { if (!retry) { setTimeout(this._unlinkChromedriverLog.bind(this, true), 1000); } } return this; }, /** * Tracks running browser processes for chrome on mac & linux * * @method _processes * @param {string} platform Current OS * @param {function} fn Callback * @chainable * @private */ _processes: function (platform, fn) { if (platform === 'win32') { this._processesWin(fn); return this; } this._processesNix(fn); return this; }, /** * Kills all associated processes * * @method _checkProcesses * @param {object|null} err Error object or null * @param {array} result List of running processes * @chainable * @private */ _checkProcesses: function (err, result) { // log that the driver shutdown process has been initiated this.reporterEvents.emit('report:log:system', 'dalek-browser-chrome: Shutting down ChromeDriver'); // kill leftover chrome browser processes result.forEach(this[(process.platform === 'win32' ? '_killWindows' : '_killNix')].bind(this)); // kill chromedriver binary this.spawned.kill('SIGTERM'); // clean up the file mess the chromedriver leaves us behind this._unlinkChromedriverLog(); return this; }, // UNIX ONLY // --------- /** * Kills a process * * @method _killNix * @param {integer} processID Process ID * @chainable * @private */ _killNix: function (processID) { var kill = true; this.openProcesses.forEach(function (pid) { if (pid === processID) { kill = false; } }); if (kill === true) { var killer = cp.spawn; killer('kill', [processID]); } return this; }, /** * Lists all chrome processes on *NIX systems * * @method _processesNix * @param {function} fn Calback * @chainable * @private */ _processesNix: function (fn) { var processName = process.platform === 'darwin' ? this.processName : this.processName.toLowerCase(); var cmd = ['ps -ax', '|', 'grep ' + processName]; cp.exec(cmd.join(' '), this._processListNix.bind(this, fn)); return this; }, /** * Deserializes a process list on nix * * @method _processListNix * @param {function} fn Calback * @param {object|null} err Error object * @param {string} stdout Output of the process list shell command * @chainable * @private */ _processListNix: function(fn, err, stdout) { var result = []; stdout.split('\n').forEach(this._splitProcessListNix.bind(this, result)); fn(err, result); return this; }, /** * Reformats the process list output on *NIX systems * * @method _splitProcessListNix * @param {array} result Reference to the process list * @param {string} line Single process in text representation * @chainable * @private */ _splitProcessListNix: function(result, line) { var data = line.split(' '); data = data.filter(this._filterProcessItemsNix.bind(this)); result.push(data[0]); return this; }, /** * Filters empty process list entries on *NIX * * @method _filterProcessItemsNix * @param {string} item Item to check * @return {string|bool} Item or falsy * @private */ _filterProcessItemsNix: function (item) { if (item !== '') { return item; } return false; }, // WIN ONLY // -------- /** * Lists all running processes (win only) * * @method _processesWin * @param {Function} callback Receives the process object as the only callback argument * @chainable * @private */ _processesWin: function (callback) { cp.exec('tasklist /FO CSV', this._processListWin.bind(this, callback)); return this; }, /** * Deserializes the process list on win * * @method _processListWin * @param {function} callback Callback to be executed after the list has been transformed * @param {object|null} err Error if error, else null * @param {string} stdout Output of the process list command * @chainable * @private */ _processListWin: function (callback, err, stdout) { var p = []; stdout.split('\r\n').forEach(this._splitProcessListWin.bind(this, p)); var proc = []; var head = null; while (p.length > 1) { var rec = p.shift(); rec = rec.replace(/\"\,/gi,'";').replace(/\"|\'/gi,'').split(';'); if (head === null){ head = rec; for (var i=0;i<head.length;i++){ head[i] = head[i].replace(/ /gi,''); } } else { var tmp = {}; for (var j=0;j<rec.length;j++){ tmp[head[j]] = rec[j].replace(/\"|\'/gi,''); } proc.push(tmp); } } var result = []; proc.forEach(this._filterProcessItemsWin.bind(this, result)); callback(null, result); return this; }, /** * Processes (transforms) the list of processes * * @method _filterProcessItemsWin * @param {array} result Reference to the result process list * @param {object} process Single piece of process information * @chainable * @private */ _filterProcessItemsWin: function (result, process) { Object.keys(process).forEach(function (key) { if (process[key] === this.processName) { result.push(process.PID); } }.bind(this)); return this; }, /** * Filters empty lines out of the process result * * @method _splitProcessListWin * @param {array} p Reference to the process list * @param {string} line Process item * @chainable * @private */ _splitProcessListWin: function (p, line) { if (line.trim().length !== 0) { p.push(line); } return this; }, /** * Kills a process (based on a PID) * that was not opened BEFORE Dalek has * been started * * @method _killWindows * @param {integer} pid Process id * @chainable * @private */ _killWindows: function (pid) { var kill = true; // check if the process was running BEFORE we started dalek this.openProcesses.forEach(function (opid) { if (opid === pid) { kill = false; } }); // kill the browser process if (kill === true) { cp.exec('taskkill /PID ' + pid + ' /f', function(){}); } return this; } }; // expose the module module.exports = ChromeDriver;