/*!
*
* 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 fs = require('fs');
var Q = require('q');
// int. libs
var WD = null;
// try/catch loading of the webdriver, so we can
// fall back to canary builds
try {
WD = require('dalek-internal-webdriver');
} catch (e) {
try {
WD = require('dalek-internal-webdriver-canary');
} catch (e) {
throw e;
}
}
/**
* Loads the webdriver client,
* launches the browser,
* initializes al object properties,
* binds to browser events
*
* @param {object} opts Options needed to kick off the driver
* @constructor
*/
var DriverNative = function (opts) {
// get the browser configuration & the browser module
var browserConf = opts.browserConf;
var browser = opts.browserMo;
// prepare properties
this._initializeProperties(opts);
// create a new webdriver client instance
this.webdriverClient = new WD(browser, this.events);
// listen on browser events
this._startBrowserEventListeners(browser);
// store desired capabilities of this session
this.desiredCapabilities = browser.desiredCapabilities;
this.browserDefaults = browser.driverDefaults;
// launch the browser & when the browser launch
// promise is fullfilled, issue the driver:ready event
// for the particular browser
browser
.launch(browserConf, this.reporterEvents, this.config)
.then(this.events.emit.bind(this.events, 'driver:ready:native:' + this.browserName, browser));
};
/**
* Launches the browsers to test
* and handles the webdriver requests & responses
*
* @module Driver
* @class DriverNative
* @namespace Dalek
* @part DriverNative
* @api
*/
DriverNative.prototype = {
/**
* Initializes the driver properties
*
* @method _initializeProperties
* @param {object} opts Options needed to kick off the driver
* @chainable
* @private
*/
_initializeProperties: function (opts) {
// prepare properties
this.actionQueue = [];
this.config = opts.config;
this.lastCalledUrl = null;
this.driverStatus = {};
this.sessionStatus = {};
// store injcted options in object properties
this.events = opts.events;
this.reporterEvents = opts.reporter;
this.browserName = opts.browser;
return this;
},
/**
* Binds listeners on browser events
*
* @method _initializeProperties
* @param {object} browser Browser module
* @chainable
* @private
*/
_startBrowserEventListeners: function (browser) {
this.reporterEvents.on('browser:notify:data:' + this.browserName, function (data) {
this.desiredCapabilities = data.desiredCapabilities;
this.browserDefaults = data.defaults;
}.bind(this));
// issue the kill command to the browser, when all tests are completed
this.events.on('tests:complete:native:' + this.browserName, browser.kill.bind(browser));
// clear the webdriver session, when all tests are completed
this.events.on('tests:complete:native:' + this.browserName, this.webdriverClient.closeSession.bind(this.webdriverClient));
return this;
},
/**
* Checks if a webdriver session has already been established,
* if not, create a new one
*
* @method start
* @return {object} promise Driver promise
*/
start: function () {
var deferred = Q.defer();
// check if a session is already active,
// if so, reuse that one
if(this.webdriverClient.hasSession()) {
deferred.resolve();
return deferred.promise;
}
// start a browser session
this._startBrowserSession(deferred, this.desiredCapabilities, this.browserDefaults);
return deferred.promise;
},
/**
* Creates a new webdriver session
* Gets the driver status
* Gets the session status
* Resolves the promise (e.g. let them tests run)
*
* @method _startBrowserSession
* @param {object} deferred Browser session deferred
* @chainable
* @private
*/
_startBrowserSession: function (deferred, desiredCapabilities, defaults) {
var viewport = this.config.get('viewport');
// start a session, transmit the desired capabilities
var promise = this.webdriverClient.createSession({desiredCapabilities: desiredCapabilities});
// set the default viewport if supported by the browser
if (defaults.viewport) {
promise = promise.then(this.webdriverClient.setWindowSize.bind(this.webdriverClient, viewport.width, viewport.height));
}
// get the driver status if supported by the browser
if (defaults.status === true) {
promise = promise
.then(this.webdriverClient.status.bind(this.webdriverClient))
.then(this._driverStatus.bind(this));
} else {
promise = promise.then(this._driverStatus.bind(this, JSON.stringify({value: defaults.status})));
}
// get the session info if supported by the browser
if (defaults.sessionInfo === true) {
promise = promise
.then(this.webdriverClient.sessionInfo.bind(this.webdriverClient))
.then(this._sessionStatus.bind(this));
} else {
promise = promise.then(this._driverStatus.bind(this, JSON.stringify({value: defaults.sessionInfo})));
}
// finally resolve the deferred
promise.then(deferred.resolve);
return this;
},
/**
* Starts to execution of a batch of tests
*
* @method end
* @chainable
*/
end: function () {
var result = Q.resolve();
// loop through all promises created by the remote methods
// this is synchronous, so it waits if a method is finished before
// the next one will be executed
this.actionQueue.forEach(function (f) {
result = result.then(f);
});
// flush the queue & fire an event
// when the queue finished its executions
result.then(this.flushQueue.bind(this));
return this;
},
/**
* Flushes the action queue (e.g. commands that should be send to the wbdriver server)
*
* @method flushQueue
* @chainable
*/
flushQueue: function () {
// clear the action queue
this.actionQueue = [];
// emit the run.complete event
this.events.emit('driver:message', {key: 'run.complete', value: null});
return this;
},
/**
* Loads the browser session status
*
* @method _sessionStatus
* @param {object} sessionInfo Session information
* @return {object} promise Browser session promise
* @private
*/
_sessionStatus: function (sessionInfo) {
var defer = Q.defer();
this.sessionStatus = JSON.parse(sessionInfo).value;
this.events.emit('driver:sessionStatus:native:' + this.browserName, this.sessionStatus);
defer.resolve();
return defer.promise;
},
/**
* Loads the browser driver status
*
* @method _driverStatus
* @param {object} statusInfo Driver status information
* @return {object} promise Driver status promise
* @private
*/
_driverStatus: function (statusInfo) {
var defer = Q.defer();
this.driverStatus = JSON.parse(statusInfo).value;
this.events.emit('driver:status:native:' + this.browserName, this.driverStatus);
defer.resolve();
return defer.promise;
},
/**
* Creates an anonymus function that calls a webdriver
* method that has no return value, emits an empty result event
* if the function has been run
* TODO: Name is weird, should be saner
*
* @method _createNonReturnee
* @param {string} fnName Name of the webdriver function that should be called
* @return {function} fn
* @private
*/
_createNonReturnee: function (fnName) {
return this._actionQueueNonReturneeTemplate.bind(this, fnName);
},
/**
* Generates a chain of webdriver calls for webdriver
* methods that don't have a return value
* TODO: Name is weird, should be saner
*
* @method _actionQueueNonReturneeTemplate
* @param {string} fnName Name of the webdriver function that should be called
* @param {string} hash Unique action hash
* @param {string} uuid Unique action hash
* @chainable
* @private
*/
_actionQueueNonReturneeTemplate:function (fnName, hash, uuid) {
this.actionQueue.push(this.webdriverClient[fnName].bind(this.webdriverClient));
this.actionQueue.push(this._generateDummyDriverMessageFn.bind(this, fnName, hash, uuid));
return this;
},
/**
* Creates a driver notification with an empty value
* TODO: Name is weird, should be saner
*
* @method _generateDummyDriverMessageFn
* @param {string} fnName Name of the webdriver function that should be called
* @param {string} hash Unique action hash
* @param {string} uuid Unique action hash
* @return {object} promise Driver message promise
* @private
*/
_generateDummyDriverMessageFn: function (fnName, hash, uuid) {
var deferred = Q.defer();
this.events.emit('driver:message', {key: fnName, value: null, uuid: uuid, hash: hash});
deferred.resolve();
return deferred.promise;
}
};
/**
* Determines if the driver is a "multi" browser driver,
* e.g. can handle more than one browser
*
* @method isMultiBrowser
* @return {bool} isMultiBrowser Driver can handle more than one browser
*/
module.exports.isMultiBrowser = function () {
return true;
};
/**
* Verifies a browser request
* TODO: Still a noop, need to add "verify the browser" logic
*
* @method verifyBrowser
* @return {bool} isVerifiedBrowser Driver can handle this browser
*/
module.exports.verifyBrowser = function () {
return true;
};
/**
* Creates a new driver instance
*
* @method create
* @param {object} opts Options needed to kick off the driver
* @return {DriverNative} driver
*/
module.exports.create = function (opts) {
// load the remote command helper methods
var dir = __dirname + '/lib/commands/';
fs.readdirSync(dir).forEach(function (file) {
require(dir + file)(DriverNative);
});
return new DriverNative(opts);
};