Plato on Github
Report Home
index.js
Maintainability
68.43
Lines of code
903
Difficulty
38.79
Estimated Errors
4.01
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 path = require('path'); var rimraf = require('rimraf'); var which = require('which').sync; var portscanner = require('portscanner'); var spawn = require('child_process').spawn; var Events = require('events').EventEmitter; // int. libs var Marionette = require('./lib/marionette'); var WebDriverServer = require('./lib/webdriver'); /** * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek). * It provides all a WebDriverServer & browser launcher for Mozilla Firefox & Firefox OS. * * The browser plugin can be installed with the following command: * * ```bash * $ npm install dalek-browser-firefox --save-dev * ``` * * You can use the browser plugin by adding a config option to the your Dalekfile * * ```javascript * "browser": ["firefox"] * ``` * * Or you can tell Dalek that it should test in this browser via the command line: * * ```bash * $ dalek mytest.js -b firefox * ``` * * Dalek looks for the browser in the std. installation directory, if you installed the * browser in a different place, you can add the location of the browser executable to you Dalekfile, * because Dalek isnʼt capable of finding the executable yet on its own. * * ```javascript * "browsers": [{ * "firefox": { * "path": "~/Apps/FirefoxNightlyDebug.app/Contents/MacOS/firefox-bin" * } * }] * ``` * * The Firefox plugin only implements a subset of Dalekʼs assertions & actions, * so if you run into any bugs, the issue is most probably related to missing commands. * Please report any issues you find, Thank you :) * * The Webdriver Server tries to open Port 9006 by default, * if this port is blocked, it tries to use a port between 9007 & 9096 * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so: * * ```javascript * "browsers": [{ * "firefox": { * "port": 5555 * } * }] * ``` * * It is also possible to specify a range of ports: * * ```javascript * "browsers": [{ * "firefox": { * "portRange": [6100, 6120] * } * }] * ``` * * If you would like to test Nightly, Aurora oder Firefox OS releases, you can simply apply a snd. argument, * which defines the browser type: * * ```bash * $ dalek mytest.js -b firefox:aurora * ``` * * for Firefox Aurora, and if you would like to use the Firefox OS, just append `:os`: * * ```bash * $ dalek mytest.js -b firefox:os * ``` * * 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": [{ * "firefox": { * "binary": "/Applications/Custom Located Firefox.app/MacOS/Contents/firefox-bin" * } * }] * ``` * * This also works for the aurora & Firefox OS builds * * ```javascript * "browsers": [{ * "firefox:aurora": { * "binary": "/Applications/Custom Located Firefox Aurora.app/MacOS/Contents/firefox-bin" * } * }] * ``` * * @module DalekJS * @class FirefoxDriver * @namespace Browser * @part Firefox * @api */ var FirefoxDriver = { /** * Verbose version of the browser name * * @property longName * @type string * @default Mozilla Firefox */ longName: 'Mozilla Firefox', /** * Default port of the FirefoxWebDriverServer * 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 9006 */ port: 9006, /** * Default maximum port of the WebDriverServer * The port is the highest port in the range that can be allocated * by the WebDriverServer * * @property maxPort * @type integer * @default 9096 */ maxPort: 9096, /** * Default port of the Marionette TCP service * The port may change, cause the port conflict resolution * tool might pick another one, if the default one is blocked * * @property marionettePort * @type integer * @default 2828 */ marionettePort: 2828, /** * Default host of the FirefoxWebDriverServer * 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: 'firefox' }, /** * 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 FirefoxWebDriverServer * * @property path * @type string * @default '' */ path: '', /** * Path to the Firefox binary file * * @property binary * @type string * @default null */ binary: null, /** * Paths to the default Firefox binary files * * @property defaultBinaries * @type object */ defaultBinaries: { default: 'firefox', darwin: '/Applications/Firefox.app/Contents/MacOS/firefox-bin', win32: process.env.ProgramFiles + '\\Mozilla Firefox\\firefox.exe', win64: process.env['ProgramFiles(x86)'] + '\\Mozilla Firefox\\firefox.exe' }, /** * Different browser types (Aurora / firefox OS) that can be controlled * via the Firefox driver * * @property browserTypes * @type object */ browserTypes: { /** * Nightly Firefox * * @property nightly * @type object */ nightly: { name: 'Firefox Nightly', linux: 'firefox', darwin: '/Applications/FirefoxNightlyDebug.app/Contents/MacOS/firefox-bin', win32: process.env.ProgramFiles + '\\Nightly\\firefox.exe', win64: process.env['ProgramFiles(x86)'] + '\\Mozilla Firefox Nightly\\firefox.exe' }, /** * Firefox Aurora * * @property aurora * @type object */ aurora: { name: 'Firefox Aurora', linux: 'firefox', darwin: '/Applications/FirefoxAuroraDebug.app/Contents/MacOS/firefox-bin', win32: process.env.ProgramFiles + '\\AuroraDebug\\firefox.exe', win64: process.env['ProgramFiles(x86)'] + '\\Mozilla AuroraDebug\\firefox.exe' }, /** * Firefox OS * * @property os * @type object */ os: { name: 'Firefox OS', linux: 'b2g', darwin: '/Applications/B2G.app/Contents/MacOS/b2g', win32: process.env.ProgramFiles + '\\B2G\\b2g.exe', win64: process.env.ProgramFiles + '\\B2G\\b2g.exe' } }, /** * Child process instance of the Firefox browser * * @property spawned * @type null|Object * @default null */ spawned: null, /** * Collected data about the created profile, * path, name, etc. * * @property profile * @type null|object * @default null */ profile: null, /** * 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; }, /** * Resolves the marionette port * * @method getMarionettePort * @return {integer} port Marionette server port */ getMarionettePort: function () { return this.marionettePort; }, /** * Returns the driver host * * @method getHost * @return {string} host WebDriver server hostname */ getHost: function () { return this.host; }, /** * Launches the FirefoxWebDriverServer, * the marionette client, & the browser. * Creates a user profile for the browser * * @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(); // init the webdriver server, // marionette client, event glue // and configuration settings configuration = this._initialize({ userconfig: configuration, reporterEvents: events, config: config, events: new Events() }); // check for a user set port var browsers = this.config.get('browsers'); if (browsers && Array.isArray(browsers)) { browsers.forEach(this._checkUserDefinedPorts.bind(this)); } // check the path of the browser binary Q.when(this.findBrowserBinary(configuration)) .then(this._afterBinaryFound.bind(this, configuration, deferred)); return deferred.promise; }, /** * Shuts down the TCP (Marionette) & HTTP connection (Webdriver), * then kills the browser process & cleans up the profiles * * @method kill * @chainable */ kill: function () { Q.all([ // shutdown Marionette client this.marionette.kill(), // shutdown webdriver server this.webDriverServer.kill() ]).then(this._killProcess.bind(this)); return this; }, /** * Locates the browser binary * * @method findBrowserBinary * @param {object} options Config options * @return {object} Promise */ findBrowserBinary: function (options) { var deferred = Q.defer(); // check if the user has set a custom binary if (options && options.binary) { this._checkUserSetBinary(options.binary, deferred); return deferred.promise; } // get the defaukt binary for this OS var defaultBinary = this._getDefaultBinary(); // check if the binary exists if (fs.existsSync(defaultBinary)) { this.binary = defaultBinary; deferred.resolve(defaultBinary); } else { var msg = 'dalek-driver-firefox: Binary not found: ' + defaultBinary; this.reporterEvents.emit('error', msg); deferred.reject({error: true, msg: msg}); } return deferred.promise; }, /** * Makes all needed modifications after we are sure if the * browser binary has been found * * @method _afterBinaryFound * @param {object} configuration User configuration * @param {object} deferred Promise * @chainable * @private */ _afterBinaryFound: function (configuration, deferred) { // generate the verbose browser name this.longName = this._modifyVerboseBrowserName(configuration); // check if we are driving a desktop browser // when, create a profile, else, not if (configuration.type !== 'os') { Q.when(this._createProfile()) .then(this._afterDesktopBinaryFound.bind(this, deferred, configuration)); } else { this._startBrowser(null, 'os', deferred); } 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 + ')'; } else { return this.longName; } }, /** * Initiates the browser startup after the desktop binary has been found * * @method _afterDesktopBinaryFound * @param {object} deferred Promise * @param {object} profile Profile data * @chainable * @private */ _afterDesktopBinaryFound: function (deferred, configuration) { this._startBrowser(this.profile.path, this.profile.name, deferred, configuration); return this; }, /** * Kills the currently running browser process * * @method _killProcess * @chainable * @priavte */ _killProcess: function () { // kill the browser process itself this.spawned.kill('SIGTERM'); // check if we need to clean up some created profiles // should always be the case except for Firefox OS if (this.profile && this.profile.path) { rimraf.sync(this.profile.path); } return this; }, /** * Initializes config & properties * * @method _initiaize * @param {object} opts Config options * @return {object} Processed userr config * @private */ _initialize: function (opts) { // store config class this.config = opts.config; // glue events this.events = opts.events; this.events.setMaxListeners(0); this.reporterEvents = opts.reporterEvents; // setup Marionette client & Webdriver server this.marionette = new Marionette(this.events); this.webDriverServer = new WebDriverServer(this.events); return opts.userconfig === null ? {} : opts.userconfig; }, /** * Get the default binary location * * @method _getDefaultBinary * @return {string} Path to binary * @private */ _getDefaultBinary: function () { var platform = process.platform; // check default binary for linuy if (platform !== 'darwin' && platform !== 'win32' && this.defaultBinaries[platform]) { return which(this.defaultBinaries.linux); } // check to see if we are on Windows x64 if (platform === 'win32' && process.arch === 'x64') { platform = 'win64'; } return this.defaultBinaries[platform] ? this.defaultBinaries[platform] : which(this.defaultBinaries.default); }, /** * Launches the browser * * @method _startBrowser * @param {string} profilePath Directory that contains the profile * @param {string} profileName Name of the profile to use * @param {object} deferred Promise * @private * @chainable */ _startBrowser: function (profilePath, profileName, deferred, configuration) { var df = Q.defer(); var args = []; // set args based on environment if (profileName !== 'os') { args = ['-marionette', '-turbo', '-profile', profilePath, '-no-remote', '-url', 'about:blank']; } // start the browser this.spawned = spawn(this.binary, args); // kind of an ugly hack, but I have no other idea to // than to wait for 2 secs to ensure Firefox runs on windows if (process.platform === 'win32' || (configuration && !configuration.type)) { this.interval = setInterval(this._scanMarionettePort.bind(this, df), 50); } else { this.spawned.stdout.on('data', this._onBrowserStartup.bind(this, df)); } // connect to the marionette socket server // and the webdriver server & resolve the promise when done df.promise.then(this._resolvePort.bind(this, deferred, profileName)); return this; }, /** * Repeatadly checks the status of the marionette port * * @method _scanMarionettePort * @param {object} df Promise * @private * @chainable */ _scanMarionettePort: function (df) { portscanner.checkPortStatus(this.getMarionettePort(), this.getHost(), this._startByInterval.bind(this, df)); return this; }, /** * Checks if the browser is available by listening on the marionette socket * * @method _startByInterval * @param {object} df Promise * @param {object} interval Reference to the portchecker interval * @param {object|null} err Error or null * @param {string} status Status of the marionette port * @private * @chainable */ _startByInterval: function (df, err, status) { if (status === 'open') { clearInterval(this.interval); df.resolve(); } return this; }, /** * Process user defined ports * * @method _checkUserDefinedPorts * @param {object} browser Browser configuration * @chainable * @private */ _checkUserDefinedPorts: function (browser) { // check for a single defined port if (browser.firefox && browser.firefox.port) { this.port = parseInt(browser.firefox.port, 10); this.maxPort = this.port + 90; this.reporterEvents.emit('report:log:system', 'dalek-browser-firefox: Switching to user defined port: ' + this.port); } // check for a port range if (browser.firefox && browser.firefox.portRange && browser.firefox.portRange.length === 2) { this.port = parseInt(browser.firefox.portRange[0], 10); this.maxPort = parseInt(browser.firefox.portRange[1], 10); this.reporterEvents.emit('report:log:system', 'dalek-browser-firefox: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort); } return this; }, /** * Resolve the WebDriverServer port * * @method _resolvePort * @param {object} deferred Promise * @param {string} profileName Name of the profile * @chainable * @private */ _resolvePort: function (deferred, profileName) { // check if the current port is in use, if so, scan for free ports portscanner.findAPortNotInUse( this.getPort(), this.getMaxPort(), this.getHost(), this._afterPortResolved.bind(this, deferred, profileName) ); return this; }, /** * Resolve the WebDriverServer port * * @method _resolvePort * @param {object} deferred Promise * @param {string} profileName Name of the profile * @param {object|null} error Error object if there is one * @param {integer} port Resolved port * @chainable * @private */ _afterPortResolved: function (deferred, profileName, error, port) { // check for errors if (error !== null && error.code !== 'ECONNREFUSED') { this.reporterEvents.emit('error', 'dalek-browser-firefox: Error starting WebDriverServer, port ' + port + ' in use'); deferred.reject(error); process.exit(); } // 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-firefox: Switching to port: ' + port); this.port = port; } // kickstart marionette client & webdriver server Q.all([ this.webDriverServer.connect(this.getPort(), this.getHost()), this.marionette.connect(this.getMarionettePort()) ]).then(this._afterConnectionHasBeenEstablished.bind(this, profileName, deferred)); return this; }, /** * Callback that will be invoked after the marionette client has * established a connection to the browser & after the webdriver server * has been launched correctly * * @method _afterConnectionHasBeenEstablished * @param {string} profileName Name of the user profile * @param {object} deferred Promise * @chainable * @private */ _afterConnectionHasBeenEstablished: function (profileName, deferred) { // Due to the lack of firefox os readiness events, // we need to wait an additional second before reporting // test readiness if (profileName === 'os') { setTimeout(deferred.resolve, 1000); } else { deferred.resolve(); } return this; }, /** * Consumes the console output when the browser is started * * @method _onBrowserStartup * @param {object} deferred Promise * @param {string} data Output from the spawned binary * @private * @chainable */ _onBrowserStartup: function (deferred, data) { if (this._browserIsReady(data)) { deferred.resolve(); } return this; }, /** * Checks if the browser is ready for communication * * @method _browserIsReady * @param {string} data Output from the spawned binary * @return {bool} true when ready, false when not * @private */ _browserIsReady: function (data) { // convert buffer to string data = data+''; // check for the ready signal on desktop firefox var desktopReady = data.search('DOMWINDOW == 12') !== -1; // check for the ready signal of the firefoxos emulator var b2gReady = data.search('BrowserElementChildPreload.js loaded') !== -1; return desktopReady || b2gReady; }, /** * Creates a new Firefox profile * * @method _createProfile * @return {Q.promise} * @private */ _createProfile: function () { var deferred = Q.defer(); var profileName = 'dalekjs-' + Math.random().toString(16).slice(2); this._createUserPreferences(deferred, profileName); return deferred.promise; }, /** * Creates user preferences for the profile * Saves them in `user.js` in the newly created profile * * @method _createProfile * @param {string} profilePath User profile directory * @return {Q.promise} * @private */ _createUserPreferences: function (deferred, profileName) { // create marionette specific user preferences var prefs = 'user_pref("browser.shell.checkDefaultBrowser", false);\n'; prefs += 'user_pref("marionette.contentListener", false);\n'; prefs += 'user_pref("marionette.defaultPrefs.enabled", true);\n'; prefs += 'user_pref("browser.shell.checkDefaultBrowser", false);\n'; prefs += 'user_pref("browser.sessionstore.resume_from_crash", false);\n'; prefs += 'user_pref("browser.bookmarks.restore_default_bookmarks", false);\n'; prefs += 'user_pref("marionette.defaultPrefs.port", "' + this.getMarionettePort() + '");\n'; // store the user preferences this.profile = {}; this.profile.path = path.join(process.cwd(), 'temp'); this.profile.name = profileName; // check if the temp dir exists, else create if (!fs.existsSync(this.profile.path)) { fs.mkdirSync(this.profile.path); } // create the preference file fs.writeFile(path.join(this.profile.path, 'prefs.js'), prefs, this._afterCreatingUserPreferences.bind(this, deferred)); return deferred.promise; }, /** * Callback that will be executed after the user preferences * have been created * * @method _afterCreatingUserPreferences * @param {object} deferred Promise * @param {object|null} err Error or null * @private * @chainable */ _afterCreatingUserPreferences: function (deferred, err) { // reject the deferred when an error occurrs if (err !== null) { this.reporterEvents.emit('error', 'dalek-browser-firefox: Error creating profile'); deferred.reject(err); process.exit(); } deferred.resolve(); return this; }, /** * Checks if the binary exists, * when set manually by the user * * @method _checkUserSetBinary * @param {string} userPath Path to the browser binary * @param {object} deferred Promise * @private * @chainable */ _checkUserSetBinary: function (userPath, deferred) { // check if we need to replace the users home directory if (process.platform === 'darwin' && userPath.trim()[0] === '~') { userPath = userPath.replace('~', process.env.HOME); } // check if the binary exists if (fs.existsSync(userPath)) { this.binary = userPath; deferred.resolve(userPath); } else { var msg = 'dalek-driver-firefox: Binary not found: ' + userPath; this.reporterEvents.emit('error', msg); process.exit(); // MAYBE switch to Daleks 'killAll' Event deferred.reject({error: true, msg: msg}); } return this; } }; module.exports = FirefoxDriver;