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