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 fs = require('fs'); |
29 | 1 | var path = require('path'); |
30 | 1 | var jsonxml = require('jsontoxml'); |
31 | | |
32 | | // int. global |
33 | 1 | var reporter = null; |
34 | | |
35 | | /** |
36 | | * The jUnit reporter can produce a jUnit compatible file with the results of your testrun, |
37 | | * this reporter enables you to use daleks testresults within a CI environment like Jenkins. |
38 | | * |
39 | | * The reporter can be installed with the following command: |
40 | | * |
41 | | * ```bash |
42 | | * $ npm install dalek-reporter-junit --save-dev |
43 | | * ``` |
44 | | * |
45 | | * The file will follow the jUnit XML format: |
46 | | * |
47 | | * ```html |
48 | | * <?xml version="1.0" encoding="utf-8"?> |
49 | | * <resource name="DalekJSTest"> |
50 | | * <testsuite start="1375125067" name="Click - DalekJS guinea pig [Phantomjs]" end="1375125067" totalTests="1"> |
51 | | * <testcase start="1375125067" name="Can click a select option (OK, jQuery style, no message)" end="1375125067" result="pass"> |
52 | | * <variation start="1375125067" name="val" end="1375125067"> |
53 | | * <severity>pass</severity> |
54 | | * <description><![CDATA[David is the favourite]]></description> |
55 | | * <resource>DalekJSTest</resource> |
56 | | * </variation> |
57 | | * <variation start="1375125067" name="val" end="1375125067"> |
58 | | * <severity>pass</severity> |
59 | | * <description><![CDATA[Matt is now my favourite, bow ties are cool]]></description> |
60 | | * <resource>DalekJSTest</resource> |
61 | | * </variation> |
62 | | * </testcase> |
63 | | * </testsuite> |
64 | | * </resource> |
65 | | * ``` |
66 | | * |
67 | | * By default the file will be written to `report/dalek.xml`, |
68 | | * you can change this by adding a config option to the your Dalekfile |
69 | | * |
70 | | * ```javascript |
71 | | * "junit-reporter": { |
72 | | * "dest": "your/folder/your_file.xml" |
73 | | * } |
74 | | * ``` |
75 | | * |
76 | | * If you would like to use the reporter (in addition to the std. console reporter), |
77 | | * you can start dalek with a special command line argument |
78 | | * |
79 | | * ```bash |
80 | | * $ dalek your_test.js -r console,junit |
81 | | * ``` |
82 | | * |
83 | | * or you can add it to your Dalekfile |
84 | | * |
85 | | * ```javascript |
86 | | * "reporter": ["console", "junit"] |
87 | | * ``` |
88 | | * |
89 | | * @class Reporter |
90 | | * @constructor |
91 | | * @part JUnit |
92 | | * @api |
93 | | */ |
94 | | |
95 | 1 | function Reporter (opts) { |
96 | 1 | this.events = opts.events; |
97 | 1 | this.config = opts.config; |
98 | 1 | this.testCount = 0; |
99 | 1 | this.testIdx = -1; |
100 | 1 | this.variationCount = -1; |
101 | 1 | this.data = {}; |
102 | 1 | this.data.tests = []; |
103 | 1 | this.browser = null; |
104 | | |
105 | 1 | var defaultReportFolder = 'report'; |
106 | 1 | this.dest = this.config.get('junit-reporter') && this.config.get('junit-reporter').dest ? this.config.get('junit-reporter').dest : defaultReportFolder; |
107 | | |
108 | | // prepare base xml |
109 | 1 | this.xml = [ |
110 | | { |
111 | | name: 'resource', |
112 | | attrs: { |
113 | | name:'DalekJSTest' |
114 | | }, |
115 | | children: [] |
116 | | } |
117 | | ]; |
118 | | |
119 | 1 | this.startListening(); |
120 | | } |
121 | | |
122 | | /** |
123 | | * @module Reporter |
124 | | */ |
125 | | |
126 | 1 | module.exports = function (opts) { |
127 | 1 | if (reporter === null) { |
128 | 1 | reporter = new Reporter(opts); |
129 | | } |
130 | | |
131 | 1 | return reporter; |
132 | | }; |
133 | | |
134 | 1 | Reporter.prototype = { |
135 | | |
136 | | /** |
137 | | * Connects to all the event listeners |
138 | | * |
139 | | * @method startListening |
140 | | * @chainable |
141 | | */ |
142 | | |
143 | | startListening: function () { |
144 | 1 | this.events.on('report:run:browser', this.runBrowser.bind(this)); |
145 | 1 | this.events.on('report:assertion', this.assertion.bind(this)); |
146 | 1 | this.events.on('report:test:started', this.testStarted.bind(this)); |
147 | 1 | this.events.on('report:test:finished', this.testFinished.bind(this)); |
148 | 1 | this.events.on('report:runner:finished', this.runnerFinished.bind(this)); |
149 | 1 | this.events.on('report:testsuite:started', this.testsuiteStarted.bind(this)); |
150 | | //this.events.on('report:testsuite:finished', this.testsuiteFinished.bind(this)); |
151 | 1 | return this; |
152 | | }, |
153 | | |
154 | | /** |
155 | | * Stores the current browser name |
156 | | * |
157 | | * @method runBrowser |
158 | | * @param {string} browser Browser name |
159 | | * @chainable |
160 | | */ |
161 | | |
162 | | runBrowser: function (browser) { |
163 | 0 | this.browser = browser; |
164 | 0 | return this; |
165 | | }, |
166 | | |
167 | | /** |
168 | | * Generates XML skeleton for testsuites |
169 | | * |
170 | | * @method testsuiteStarted |
171 | | * @param {string} name Testsuite name |
172 | | * @chainable |
173 | | */ |
174 | | |
175 | | testsuiteStarted: function (name) { |
176 | 0 | this.testCount = 0; |
177 | 0 | this.testIdx++; |
178 | 0 | this.xml[0].children.push({ |
179 | | name: 'testsuite', |
180 | | children: [], |
181 | | attrs: { |
182 | | name: name + ' [' + this.browser + ']', |
183 | | } |
184 | | }); |
185 | 0 | return this; |
186 | | }, |
187 | | |
188 | | /** |
189 | | * Finishes XML skeleton for testsuites |
190 | | * |
191 | | * @method testsuiteFinished |
192 | | * @chainable |
193 | | */ |
194 | | |
195 | | testsuiteFinished: function () { |
196 | | // this.xml[0].children[this.testIdx].attrs.end = Math.round(new Date().getTime() / 1000); |
197 | 0 | return this; |
198 | | }, |
199 | | |
200 | | /** |
201 | | * Generates XML skeleton for an assertion |
202 | | * |
203 | | * @method assertion |
204 | | * @param {object} data Event data |
205 | | * @chainable |
206 | | */ |
207 | | |
208 | | assertion: function (data) { |
209 | 0 | if (! data.success) { |
210 | | //var timestamp = Math.round(new Date().getTime() / 1000); |
211 | 0 | this.xml[0].children[this.testIdx].children[this.testCount].children.push({ |
212 | | name: 'failure', |
213 | | attrs: { |
214 | | name: data.type, |
215 | | message: (data.message ? data.message : 'Expected: ' + data.expected + 'Actual: ' + data.value) |
216 | | } |
217 | | }); |
218 | | |
219 | | //if (this.variationCount > -1 && this.xml[0].children[this.testIdx].children[this.testCount].children[this.variationCount]) { |
220 | | //this.xml[0].children[this.testIdx].children[this.testCount].children[this.variationCount].attrs.end = timestamp; |
221 | | //} |
222 | | |
223 | 0 | this.variationCount++; |
224 | | } |
225 | | |
226 | 0 | return this; |
227 | | }, |
228 | | |
229 | | /** |
230 | | * Generates XML skeleton for a testcase |
231 | | * |
232 | | * @method testStarted |
233 | | * @param {object} data Event data |
234 | | * @chainable |
235 | | */ |
236 | | |
237 | | testStarted: function (data) { |
238 | 0 | this.variationCount = -1; |
239 | 0 | this.xml[0].children[this.testIdx].children.push({ |
240 | | name: 'testcase', |
241 | | children: [], |
242 | | attrs: { |
243 | | classname: this.xml[0].children[this.testIdx].attrs.name, |
244 | | name: data.name |
245 | | } |
246 | | }); |
247 | | |
248 | 0 | return this; |
249 | | }, |
250 | | |
251 | | /** |
252 | | * Finishes XML skeleton for a testcase |
253 | | * |
254 | | * @method testFinished |
255 | | * @param {object} data Event data |
256 | | * @chainable |
257 | | */ |
258 | | |
259 | | testFinished: function () { |
260 | | //var timestamp = Math.round(new Date().getTime() / 1000); |
261 | | |
262 | 0 | if (this._checkNodeAttributes(this.testIdx, this.testCount)) { |
263 | 0 | this.xml[0].children[this.testIdx].children[this.testCount].attrs = {}; |
264 | | } |
265 | | //this.xml[0].children[this.testIdx].children[this.testCount].attrs.end = timestamp; |
266 | | //this.xml[0].children[this.testIdx].children[this.testCount].attrs.result = data.status ? 'Passed' : 'Failed'; |
267 | | |
268 | 0 | if (this.variationCount > -1) { |
269 | 0 | if (this._checkNodeAttributes(this.testIdx, this.testCount, this.variationCount)) { |
270 | 0 | this.xml[0].children[this.testIdx].children[this.testCount].children[this.variationCount].attrs = {}; |
271 | | } |
272 | | //this.xml[0].children[this.testIdx].children[this.testCount].children[this.variationCount].attrs.end = timestamp; |
273 | | } |
274 | | |
275 | 0 | this.testCount++; |
276 | 0 | this.variationCount = -1; |
277 | 0 | return this; |
278 | | }, |
279 | | |
280 | | /** |
281 | | * Finishes XML and writes file to the file system |
282 | | * |
283 | | * @method runnerFinished |
284 | | * @param {object} data Event data |
285 | | * @chainable |
286 | | */ |
287 | | |
288 | | runnerFinished: function (data) { |
289 | 0 | this.data.elapsedTime = data.elapsedTime; |
290 | 0 | this.data.status = data.status; |
291 | 0 | this.data.assertions = data.assertions; |
292 | 0 | this.data.assertionsFailed = data.assertionsFailed; |
293 | 0 | this.data.assertionsPassed = data.assertionsPassed; |
294 | | |
295 | 0 | var contents = jsonxml(this.xml, {escape: true, removeIllegalNameCharacters: true, prettyPrint: true, xmlHeader: 'version="1.0" encoding="UTF-8"'}); |
296 | | |
297 | 0 | if (path.extname(this.dest) !== '.xml') { |
298 | 0 | this.dest = this.dest + '/dalek.xml'; |
299 | | } |
300 | | |
301 | 0 | this.events.emit('report:written', {type: 'junit', dest: this.dest}); |
302 | 0 | this._recursiveMakeDirSync(path.dirname(this.dest.replace(path.basename(this.dest, '')))); |
303 | 0 | fs.writeFileSync(this.dest, contents, 'utf8'); |
304 | | }, |
305 | | |
306 | | /** |
307 | | * Helper method to generate deeper nested directory structures |
308 | | * |
309 | | * @method _recursiveMakeDirSync |
310 | | * @param {string} path PAth to create |
311 | | */ |
312 | | |
313 | | _recursiveMakeDirSync: function (path) { |
314 | 0 | var pathSep = require('path').sep; |
315 | 0 | var dirs = path.split(pathSep); |
316 | 0 | var root = ''; |
317 | | |
318 | 0 | while (dirs.length > 0) { |
319 | 0 | var dir = dirs.shift(); |
320 | 0 | if (dir === '') { |
321 | 0 | root = pathSep; |
322 | | } |
323 | 0 | if (!fs.existsSync(root + dir)) { |
324 | 0 | fs.mkdirSync(root + dir); |
325 | | } |
326 | 0 | root += dir + pathSep; |
327 | | } |
328 | | }, |
329 | | |
330 | | /** |
331 | | * Helper method to check if attributes should be set to an empty object literal |
332 | | * |
333 | | * @method _checkNodeAttributes |
334 | | * @param {string} testIdx Id of the test node |
335 | | * @param {string} testCount Id of the child node |
336 | | * @param {string} variationCount Id of the testCount child node |
337 | | */ |
338 | | |
339 | | _checkNodeAttributes: function (testIdx, testCount, variationCount) { |
340 | 0 | if (variationCount === undefined) { |
341 | 0 | return typeof this.xml[0].children[testIdx].children[testCount].attrs === 'undefined'; |
342 | | } |
343 | | |
344 | 0 | return typeof this.xml[0].children[testIdx].children[testCount].children[variationCount].attrs === 'undefined'; |
345 | | } |
346 | | }; |
347 | | |