/*!
*
* 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 _ = require('lodash');
var fs = require('fs');
var EventEmitter2 = require('eventemitter2').EventEmitter2;
// int. libs
var unit = require('./unit');
/**
* @constructor
* @param {object} options
*/
var Suite = function (options) {
this.emitter = new EventEmitter2();
this.emitter.setMaxListeners(Infinity);
this.initialize(options);
this.suite = this.loadTestsuite(options.file);
};
/**
* Suite (Testsuite)
*
* @module DalekJS
* @class Suite
* @namespace Dalek
* @part Testsuite
* @api
*/
Suite.prototype = {
/**
* Assigns the initial options
* driverEmitter -> the drivers event dispatcher
* reporterEmitter -> the reporters event dispatcher
* driver -> the driver instance (e.g. native webdriver, selenium, etc.)
* name -> the suites filename (default suite name)
*
* @method initialize
* @param {object} options
* @chainable
*/
initialize: function (options) {
this.driverEmitter = options.driverEmitter;
this.reporterEmitter = options.reporterEmitter;
this.driver = options.driver;
this.name = options.file;
this.numberOfSuites = options.numberOfSuites;
this.error = null;
return this;
},
/**
* Loads the testsuite that should be executed
*
* @method loadTestsuite
* @param {string} testfile
* @return {object} testsuite
*/
loadTestsuite: function (testfile) {
var suite = {};
// if the tests were passed in *as* a list of tests, just use them
if (testfile && typeof testfile === 'object') {
var allAreTestFunctions = true;
for (var testname in testfile) {
if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; }
}
if (allAreTestFunctions) {
return testfile;
}
}
// catch any errors, like falsy requires & stuff
try {
if (fs.existsSync(process.cwd() + '/' + testfile)) {
suite = require(process.cwd() + '/' + testfile.replace('.js', ''));
} else {
this.error = 'Suite "' + testfile + '" does not exist. Skipping!';
return suite;
}
} catch (e) {
this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e;
return suite;
}
suite._uid = _.uniqueId('Suite');
return suite;
},
/**
* Checks if all tests from the testsuite are executed.
* Runs the next test if not.
* Triggers `asyncs` callback if the suite is finished.
* Decrements the `testsToBeExecuted` counter
*
* @method testFinished
* @param {function} callback
* @param {array} tests
* @param {object} test
* @param {string} event
* @chainable
*/
testFinished: function (callback, tests) {
var complete = function() {
// check if there are still tests that should be executed in this suite,
// if so, run them
if (this.decrementTestsToBeExecuted() > 1) {
this.executeNextTest(tests);
return this;
}
// run a function after the testsuite, if given
if (this.options.teardown) {
this.options.teardown();
}
// emit the suite finished event
this.reporterEmitter.emit('report:testsuite:finished', this.name);
// move on to the next suite
callback();
return this;
}.bind(this);
// run a function after the test, if given
if (typeof this.options.afterEach === 'function') {
// If there is an argument, assume async.
if (this.options.afterEach.length === 1) {
this.options.afterEach(function() {
return complete();
}.bind(this));
} else {
// no argument, assume sync.
this.options.afterEach();
return complete();
}
} else {
return complete();
}
},
/**
* Decrements number of tests that should be executed in this suite
*
* @method decrementTestsToBeExecuted
* @return {integer} numberOfTestsToBeExecuted
*/
decrementTestsToBeExecuted: function () {
return (this.testsToBeExecuted--) -1;
},
/**
* Returns the name of the testsuite
* If the suite has no name, it will return the testsuites filename
*
* @method getName
* @return {string} name
*/
getName: function () {
if (this.suite.name && _.isString(this.suite.name)) {
var name = this.suite.name;
delete this.suite.name;
return name;
}
return this.name;
},
/**
* Returns the options of the testsuite
* If the suite has no options, it will return an empty object
*
* @method getOptions
* @return {object} options Suite options
*/
getOptions: function () {
if (this.suite.options && _.isObject(this.suite.options)) {
var options = this.suite.options;
delete this.suite.options;
return options;
}
return {};
},
/**
* Returns all names (aka. object keys) the tests that should be executed
*
* @method getTests
* @return {array} test
*/
getTests: function () {
return Object.keys(this.suite);
},
/**
* Returns the number of tests to be executed
*
* @method getNumberOfTests
* @param {array} tests
* @return {integer} numberOfTests
*/
getNumberOfTests: function (tests) {
return tests.length;
},
/**
* Returns the next test, that should be executed
*
* @method getNextTest
* @return {string} testName
*/
getNextTest: function (tests) {
return tests.shift();
},
/**
* Executes the next test in the sequence
*
* @method executeNextTest
* @param {array} tests
* @return {mixed} testValue
*/
executeNextTest: function (tests, callback) {
var cb = callback || function() {};
// grab the next test in the queue
var testName = this.getNextTest(tests);
// get the next test function
var testFunction = this.getTest(testName);
// generate an instance of the test
var test = this.getTestInstance(testName);
// run a setup function before the test, if given
if (typeof this.options.beforeEach !== 'function') {
cb(null, null);
testFunction.apply(test,[test]);
return this;
}
if (this.options.beforeEach.length === 1) {
// if function takes an argument, assume async
this.options.beforeEach(function() {
// start it
testFunction.apply(test,[test]);
cb(null, null);
});
} else {
// otherwise, assume sync
this.options.beforeEach();
testFunction.apply(test,[test]);
cb(null, null);
}
return this;
},
/**
* Generates a new test instance
*
* @method getTestInstance
* @param {string} name
* @return {Dalek.Test} test
*/
getTestInstance: function (name) {
return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name});
},
/**
* Returns a test function by its name
*
* @method getTest
* @param {string} name
* @return {function} test
*/
getTest: function (name) {
return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist;
},
/**
* Will be executed if a test is started, that does not exist
*
* @method testDoesNotExist
* @param {object} options
*/
testDoesNotExist: function (options) {
if (options.name) {
this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.');
}
return this;
},
/**
* Runs any tests from this testsuite in sequence
*
* @method run
* @param {function} callback
* @chainable
*/
run: function (callback) {
var tests = [];
// check if the suite is
if (this.error) {
this.reporterEmitter.emit('report:testsuite:started', null);
// emit a warning notice
this.reporterEmitter.emit('warning', this.error);
// emit the suite finished event
this.reporterEmitter.emit('report:testsuite:finished', null);
// move on to the next suite
callback();
}
// extract suite name
this.name = this.getName();
// extract suite options
this.options = this.getOptions();
// extract tests
tests = this.getTests();
this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests);
// run a function before the testsuite has been launched, if given
if (this.options.setup) {
this.options.setup();
}
// kickstart the test execution
this.executeNextTest(tests, function() {
// emit the suite started event
this.reporterEmitter.emit('report:testsuite:started', this.name);
// listen to the test:finished event & then start the next test
// if there are no tests in this suite left,
// run the async callback & mark this suite as finished
this.emitter.onAny(this.testFinished.bind(this, callback, tests));
}.bind(this));
return this;
}
};
// export the testuite instance
module.exports = Suite;