API Docs for: 0.0.9
Show:

File: lib/dalek/suite.js

  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.  
  25. 'use strict';
  26.  
  27. // ext. libs
  28. var _ = require('lodash');
  29. var fs = require('fs');
  30. var EventEmitter2 = require('eventemitter2').EventEmitter2;
  31.  
  32. // int. libs
  33. var unit = require('./unit');
  34.  
  35. /**
  36. * @constructor
  37. * @param {object} options
  38. */
  39.  
  40. var Suite = function (options) {
  41. this.emitter = new EventEmitter2();
  42. this.emitter.setMaxListeners(Infinity);
  43. this.initialize(options);
  44. this.suite = this.loadTestsuite(options.file);
  45. };
  46.  
  47. /**
  48. * Suite (Testsuite)
  49. *
  50. * @module DalekJS
  51. * @class Suite
  52. * @namespace Dalek
  53. * @part Testsuite
  54. * @api
  55. */
  56.  
  57. Suite.prototype = {
  58.  
  59. /**
  60. * Assigns the initial options
  61. * driverEmitter -> the drivers event dispatcher
  62. * reporterEmitter -> the reporters event dispatcher
  63. * driver -> the driver instance (e.g. native webdriver, selenium, etc.)
  64. * name -> the suites filename (default suite name)
  65. *
  66. * @method initialize
  67. * @param {object} options
  68. * @chainable
  69. */
  70.  
  71. initialize: function (options) {
  72. this.driverEmitter = options.driverEmitter;
  73. this.reporterEmitter = options.reporterEmitter;
  74. this.driver = options.driver;
  75. this.name = options.file;
  76. this.numberOfSuites = options.numberOfSuites;
  77. this.error = null;
  78. return this;
  79. },
  80.  
  81. /**
  82. * Loads the testsuite that should be executed
  83. *
  84. * @method loadTestsuite
  85. * @param {string} testfile
  86. * @return {object} testsuite
  87. */
  88.  
  89. loadTestsuite: function (testfile) {
  90. var suite = {};
  91.  
  92. // if the tests were passed in *as* a list of tests, just use them
  93. if (testfile && typeof testfile === 'object') {
  94. var allAreTestFunctions = true;
  95. for (var testname in testfile) {
  96. if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; }
  97. }
  98. if (allAreTestFunctions) {
  99. return testfile;
  100. }
  101. }
  102.  
  103. // catch any errors, like falsy requires & stuff
  104. try {
  105.  
  106. if (fs.existsSync(process.cwd() + '/' + testfile)) {
  107. suite = require(process.cwd() + '/' + testfile.replace('.js', ''));
  108. } else {
  109. this.error = 'Suite "' + testfile + '" does not exist. Skipping!';
  110. return suite;
  111. }
  112. } catch (e) {
  113. this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e;
  114. return suite;
  115. }
  116.  
  117. suite._uid = _.uniqueId('Suite');
  118. return suite;
  119. },
  120.  
  121. /**
  122. * Checks if all tests from the testsuite are executed.
  123. * Runs the next test if not.
  124. * Triggers `asyncs` callback if the suite is finished.
  125. * Decrements the `testsToBeExecuted` counter
  126. *
  127. * @method testFinished
  128. * @param {function} callback
  129. * @param {array} tests
  130. * @param {object} test
  131. * @param {string} event
  132. * @chainable
  133. */
  134.  
  135. testFinished: function (callback, tests) {
  136. var complete = function() {
  137. // check if there are still tests that should be executed in this suite,
  138. // if so, run them
  139. if (this.decrementTestsToBeExecuted() > 1) {
  140. this.executeNextTest(tests);
  141. return this;
  142. }
  143.  
  144. // run a function after the testsuite, if given
  145. if (this.options.teardown) {
  146. this.options.teardown();
  147. }
  148.  
  149. // emit the suite finished event
  150. this.reporterEmitter.emit('report:testsuite:finished', this.name);
  151.  
  152. // move on to the next suite
  153. callback();
  154. return this;
  155. }.bind(this);
  156.  
  157. // run a function after the test, if given
  158.  
  159. if (typeof this.options.afterEach === 'function') {
  160. // If there is an argument, assume async.
  161. if (this.options.afterEach.length === 1) {
  162. this.options.afterEach(function() {
  163. return complete();
  164. }.bind(this));
  165. } else {
  166. // no argument, assume sync.
  167. this.options.afterEach();
  168. return complete();
  169. }
  170. } else {
  171. return complete();
  172. }
  173.  
  174. },
  175.  
  176. /**
  177. * Decrements number of tests that should be executed in this suite
  178. *
  179. * @method decrementTestsToBeExecuted
  180. * @return {integer} numberOfTestsToBeExecuted
  181. */
  182.  
  183. decrementTestsToBeExecuted: function () {
  184. return (this.testsToBeExecuted--) -1;
  185. },
  186.  
  187. /**
  188. * Returns the name of the testsuite
  189. * If the suite has no name, it will return the testsuites filename
  190. *
  191. * @method getName
  192. * @return {string} name
  193. */
  194.  
  195. getName: function () {
  196. if (this.suite.name && _.isString(this.suite.name)) {
  197. var name = this.suite.name;
  198. delete this.suite.name;
  199. return name;
  200. }
  201.  
  202. return this.name;
  203. },
  204.  
  205. /**
  206. * Returns the options of the testsuite
  207. * If the suite has no options, it will return an empty object
  208. *
  209. * @method getOptions
  210. * @return {object} options Suite options
  211. */
  212.  
  213. getOptions: function () {
  214. if (this.suite.options && _.isObject(this.suite.options)) {
  215. var options = this.suite.options;
  216. delete this.suite.options;
  217. return options;
  218. }
  219.  
  220. return {};
  221. },
  222.  
  223. /**
  224. * Returns all names (aka. object keys) the tests that should be executed
  225. *
  226. * @method getTests
  227. * @return {array} test
  228. */
  229.  
  230. getTests: function () {
  231. return Object.keys(this.suite);
  232. },
  233.  
  234. /**
  235. * Returns the number of tests to be executed
  236. *
  237. * @method getNumberOfTests
  238. * @param {array} tests
  239. * @return {integer} numberOfTests
  240. */
  241.  
  242. getNumberOfTests: function (tests) {
  243. return tests.length;
  244. },
  245.  
  246. /**
  247. * Returns the next test, that should be executed
  248. *
  249. * @method getNextTest
  250. * @return {string} testName
  251. */
  252.  
  253. getNextTest: function (tests) {
  254. return tests.shift();
  255. },
  256.  
  257. /**
  258. * Executes the next test in the sequence
  259. *
  260. * @method executeNextTest
  261. * @param {array} tests
  262. * @return {mixed} testValue
  263. */
  264.  
  265. executeNextTest: function (tests, callback) {
  266. var cb = callback || function() {};
  267. // grab the next test in the queue
  268. var testName = this.getNextTest(tests);
  269. // get the next test function
  270. var testFunction = this.getTest(testName);
  271. // generate an instance of the test
  272. var test = this.getTestInstance(testName);
  273. // run a setup function before the test, if given
  274. if (typeof this.options.beforeEach !== 'function') {
  275. cb(null, null);
  276. testFunction.apply(test,[test]);
  277. return this;
  278. }
  279. if (this.options.beforeEach.length === 1) {
  280. // if function takes an argument, assume async
  281. this.options.beforeEach(function() {
  282. // start it
  283. testFunction.apply(test,[test]);
  284. cb(null, null);
  285. });
  286. } else {
  287. // otherwise, assume sync
  288. this.options.beforeEach();
  289. testFunction.apply(test,[test]);
  290. cb(null, null);
  291. }
  292. return this;
  293. },
  294.  
  295. /**
  296. * Generates a new test instance
  297. *
  298. * @method getTestInstance
  299. * @param {string} name
  300. * @return {Dalek.Test} test
  301. */
  302.  
  303. getTestInstance: function (name) {
  304. return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name});
  305. },
  306.  
  307. /**
  308. * Returns a test function by its name
  309. *
  310. * @method getTest
  311. * @param {string} name
  312. * @return {function} test
  313. */
  314.  
  315. getTest: function (name) {
  316. return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist;
  317. },
  318.  
  319. /**
  320. * Will be executed if a test is started, that does not exist
  321. *
  322. * @method testDoesNotExist
  323. * @param {object} options
  324. */
  325.  
  326. testDoesNotExist: function (options) {
  327. if (options.name) {
  328. this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.');
  329. }
  330. return this;
  331. },
  332.  
  333. /**
  334. * Runs any tests from this testsuite in sequence
  335. *
  336. * @method run
  337. * @param {function} callback
  338. * @chainable
  339. */
  340.  
  341. run: function (callback) {
  342. var tests = [];
  343.  
  344. // check if the suite is
  345. if (this.error) {
  346. this.reporterEmitter.emit('report:testsuite:started', null);
  347. // emit a warning notice
  348. this.reporterEmitter.emit('warning', this.error);
  349. // emit the suite finished event
  350. this.reporterEmitter.emit('report:testsuite:finished', null);
  351. // move on to the next suite
  352. callback();
  353. }
  354.  
  355. // extract suite name
  356. this.name = this.getName();
  357. // extract suite options
  358. this.options = this.getOptions();
  359.  
  360. // extract tests
  361. tests = this.getTests();
  362. this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests);
  363.  
  364. // run a function before the testsuite has been launched, if given
  365. if (this.options.setup) {
  366. this.options.setup();
  367. }
  368.  
  369. // kickstart the test execution
  370. this.executeNextTest(tests, function() {
  371. // emit the suite started event
  372. this.reporterEmitter.emit('report:testsuite:started', this.name);
  373. // listen to the test:finished event & then start the next test
  374. // if there are no tests in this suite left,
  375. // run the async callback & mark this suite as finished
  376. this.emitter.onAny(this.testFinished.bind(this, callback, tests));
  377. }.bind(this));
  378.  
  379. return this;
  380. }
  381. };
  382.  
  383. // export the testuite instance
  384. module.exports = Suite;
  385.