UNPKG

23.6 kBJavaScriptView Raw
1import events from 'events'
2import Allure from 'allure-js-commons-workflo'
3import Step from 'allure-js-commons-workflo/beans/step'
4import * as fs from 'fs'
5import * as path from 'path'
6
7function isEmpty (object) {
8 return !object || Object.keys(object).length === 0
9}
10
11const LOGGING_HOOKS = ['"before all" hook', '"after all" hook']
12
13let debug = false
14let debugSeleniumCommand = false
15
16function logger () {
17 if (debug) {
18 console.log(arguments)
19 }
20}
21function error () {
22 console.error(arguments)
23}
24
25/**
26 * Initialize a new `Allure` test reporter.
27 *
28 * @param {Runner} runner
29 * @api public
30 */
31class AllureReporter extends events.EventEmitter {
32 constructor (baseReporter, config, options = {}) {
33 super()
34
35 this.baseReporter = baseReporter
36 this.config = config
37 this.options = options
38 if (options) {
39 debug = options.debug
40 debugSeleniumCommand = options.debugSeleniumCommand
41 }
42 this.allures = {}
43 this.currentTestId = ''
44 this.startedSpecs = false
45 this.testIds = {}
46 this.errorCtr = 0
47
48 // const { epilogue } = this.baseReporter
49
50 this.on('end', () => {
51 // epilogue.call(baseReporter)
52 if (this.startedSpecs) {
53 this.addResults()
54 }
55 })
56
57 this.on('startSpecs', (runner) => {
58 if (!this.startedSpecs) {
59 this.startedSpecs = true
60 }
61 })
62
63 this.on('suite:start', (suite) => {
64 const allure = this.getAllure(suite.cid)
65 const currentSuite = allure.getCurrentSuite()
66 const prefix = currentSuite ? currentSuite.name + ' ' : ''
67
68 process.workflo.currentCid = suite.cid
69
70 allure.startSuite(prefix + suite.title)
71 })
72
73 this.on('suite:end', (suite) => {
74 this.getAllure(suite.cid).endSuite()
75 })
76
77 this.on('test:setCurrentId', (test) => {
78 const allure = this.getAllure(test.cid)
79 this.currentTestId = test.id
80
81 const currentTest = allure.getCurrentTest()
82
83 if (this.currentTestId) {
84 if (test.testcase) {
85 const traceInfo = this.config.traceInfo.testcases[this.currentTestId]
86
87 currentTest.addParameter('argument', 'Testcase File', traceInfo.testcaseFile)
88 currentTest.addParameter('argument', 'Verifies Specs', traceInfo.specs.join(', '))
89 } else if (test.spec) {
90 const parts = this.currentTestId.split('|')
91 const spec = parts[0]
92 const criteria = parts[1]
93
94 const traceInfo = this.config.traceInfo.specs[spec]
95
96 currentTest.addParameter('argument', 'Spec File', traceInfo.specFile)
97
98 const manualFile = traceInfo.criteriaVerificationFiles[criteria].manualFile
99 const testcases = traceInfo.criteriaVerificationFiles[criteria].testcases
100
101 if (manualFile) {
102 currentTest.addParameter('argument', 'Verified by Manual Results', manualFile)
103 } else if (testcases.length > 0) {
104 currentTest.addParameter('argument', 'Verified in Testcases', testcases.join(', '))
105 }
106 }
107 }
108 })
109
110 this.on('test:start', (test) => {
111 const allure = this.getAllure(test.cid)
112 allure.startCase(test.title)
113
114 this.lastCid = test.cid
115
116 const currentTest = allure.getCurrentTest()
117
118 if (test.metadata) {
119 for (const key in test.metadata) {
120 const meta = {
121 event: 'test:meta',
122 cid: test.cid
123 }
124 meta[key] = test.metadata[key]
125
126 this.handleMetadata(meta)
127 }
128 }
129
130 // add spec ids
131 let printObject = this.config.printObject
132
133 const filterFilesStr = `${printObject['Spec Files']} specFiles, ${printObject['Testcase Files']} testcaseFiles, ${printObject['Manual Result Files']} manualResultFiles`
134 const filtersStr = `${printObject['Features']} features, ${printObject['Specs']} specs, ${printObject['Suites']} suites, ${printObject['Testcases']} testcases`
135 const coverageStr = `${printObject['Automated Criteria'].count} automated ${printObject['Automated Criteria'].percentage}, ${printObject['Manual Criteria'].count} manual ${printObject['Manual Criteria'].percentage}, ${printObject['Uncovered Criteria'].count} uncovered ${printObject['Uncovered Criteria'].percentage}`
136 let uncoveredCriteriaStr = ''
137
138 for (const spec in printObject['Uncovered Criteria Object']) {
139 uncoveredCriteriaStr += `${spec}: [${printObject['Uncovered Criteria Object'][spec].join(', ')}], `
140 }
141
142 uncoveredCriteriaStr = uncoveredCriteriaStr.substring(0, uncoveredCriteriaStr.length - 2)
143
144 // these will be added on dashboard page
145 currentTest.addParameter('environment-variable', 'BROWSER', test.runner[test.cid].browserName)
146 currentTest.addParameter('environment-variable', 'BASE URL', this.config.baseUrl)
147
148 if (this.config.manualOnly) {
149 currentTest.addParameter('environment-variable', 'EXECUTION MODE: ', 'Manual Only')
150 } else if (this.config.automaticOnly) {
151 currentTest.addParameter('environment-variable', 'EXECUTION MODE: ', 'Automatic Only')
152 } else {
153 currentTest.addParameter('environment-variable', 'EXECUTION MODE: ', 'Automatic and Manual')
154 }
155
156 // currentTest.addParameter('environment-variable', 'SELENIUM CAPABILITIES', JSON.stringify(test.runner[test.cid]))
157 // currentTest.addParameter('environment-variable', 'WEBDRIVER HOST', this.config.host)
158 // currentTest.addParameter('environment-variable', 'WEBDRIVER PORT', this.config.port)
159 currentTest.addParameter('environment-variable', 'CRITERIA COVERAGE: ', coverageStr)
160 currentTest.addParameter('environment-variable', 'FILTER FILES: ', filterFilesStr)
161 currentTest.addParameter('environment-variable', 'FILTERS: ', filtersStr)
162
163 if (uncoveredCriteriaStr) {
164 currentTest.addParameter('environment-variable', 'UNCOVERED CRITERIA: ', uncoveredCriteriaStr)
165 }
166 })
167
168 this.on('test:pass', (test) => {
169 const allure = this.getAllure(test.cid)
170
171 this.addArguments(test)
172 this.addResultsDummy(test)
173 allure.endCase('passed')
174 })
175
176 this.on('test:fail', (test) => {
177 const allure = this.getAllure(test.cid)
178
179 if (!allure.getCurrentTest()) {
180 allure.startCase(test.title)
181 } else {
182 allure.getCurrentTest().name = test.title
183 }
184
185 while (allure.getCurrentSuite().currentStep instanceof Step) {
186 allure.endStep('failed')
187 }
188
189 this.addArguments(test)
190 this.addResultsDummy(test)
191 allure.endCase('failed', {
192 message: '\n' + test.err.message,
193 stack: '\n' + test.err.stack
194 })
195 })
196
197 this.on('test:broken', (test) => {
198 const allure = this.getAllure(test.cid)
199
200 if (!allure.getCurrentTest()) {
201 allure.startCase(test.title)
202 } else {
203 allure.getCurrentTest().name = test.title
204 }
205
206 while (allure.getCurrentSuite().currentStep instanceof Step) {
207 allure.endStep('broken')
208 }
209
210 this.addArguments(test)
211 this.addResultsDummy(test)
212
213 allure.endCase('broken', {
214 message: '\n' + test.err.message,
215 stack: '\n' + test.err.stack
216 })
217 })
218
219 this.on('test:pending', (test) => {
220 const allure = this.getAllure(test.cid)
221
222 this.addArguments(test)
223 this.addResultsDummy(test)
224 allure.endCase('pending', {message: 'Test ignored', stack: ''})
225 })
226
227 this.on('test:unverified', (test) => {
228 const allure = this.getAllure(test.cid)
229
230 if (!allure.getCurrentTest()) {
231 allure.startCase(test.title)
232 } else {
233 allure.getCurrentTest().name = test.title
234 }
235
236 this.addArguments(test)
237 this.addResultsDummy(test)
238 allure.endCase('unknown', {
239 message: '\n' + test.err.message
240 })
241 })
242
243 this.on('runner:command', (command) => {
244 const allure = this.getAllure(command.cid)
245
246 if (!this.isAnyTestRunning(allure)) {
247 return
248 }
249
250 if (debugSeleniumCommand) {
251 allure.startStep(`${command.method} ${command.uri.path}`)
252
253 if (!isEmpty(command.data)) {
254 this.dumpJSON(allure, 'Request', command.data)
255 }
256 }
257 })
258
259 this.on('runner:result', (command) => {
260 const allure = this.getAllure(command.cid)
261 let status = 'passed'
262
263 if (!this.isAnyTestRunning(allure)) {
264 return
265 }
266
267 if (debugSeleniumCommand) {
268 if (command.body.screenshot) {
269 allure.addAttachment('Screenshot', new Buffer(command.body.screenshot, 'base64'))
270 delete command.body.screenshot
271 }
272 if (command.body.type && command.body.type === 'RuntimeError') {
273 status = 'broken'
274 }
275
276 this.dumpJSON(allure, 'Response', command.body)
277
278 allure.endStep(status)
279 }
280 })
281
282 /*
283 meta : {
284 (mandatory) cid : ...,
285 (mandatory) event : 'test:meta'
286 (optional) description : string
287 (optional) feature : string | array
288 (optional) story : string | array
289 (optional) issue : string | array // bugs???
290 (optional) severity : [ 'blocker','critical','normal','minor','trivial' ]
291 (optional) argument : {name: value, name2: value }
292 (optional) environment : {name: value, name2: value }
293 }
294 */
295 this.on('test:meta', function (meta) {
296 this.handleMetadata(meta)
297 })
298
299 this.on('step:start', function (step) {
300 const allure = this.getAllure(step.cid)
301 logger('step:start', step)
302
303 if (!this.isAnyTestRunning(allure)) {
304 error('ERROR', 'cannot start step because no test is running', step)
305 return
306 }
307 allure.startStep(step.title != null ? step.title : 'No name defined')
308
309 if (step.arg) {
310 this.dumpJSON(allure, 'Step Arg', step.arg)
311 }
312 })
313
314 this.on('step:end', function (step) {
315 const allure = this.getAllure(step.cid)
316 logger('step:end', step)
317
318 if (!this.isAnyTestRunning(allure)) {
319 error('ERROR', 'cannot end step because no test is running', step)
320 return
321 }
322
323 if (step.arg) {
324 this.dumpJSON(allure, 'Step Result', step.arg)
325 }
326
327 if (step.assertionFailures) {
328 this.dumpJSON(allure, 'Verification Failures', step.assertionFailures)
329 }
330
331 if (step.screenshots) {
332 for (const screenshotMessage in step.screenshots) {
333 for (let i = 0; i < step.screenshots[screenshotMessage].length; ++i) {
334 allure.addAttachment(`${screenshotMessage} (${(i + 1)})`, new Buffer(fs.readFileSync(step.screenshots[screenshotMessage][i], {encoding: 'base64'}), 'base64'))
335 }
336 }
337 }
338
339 allure.endStep(step.status)
340 })
341
342 this.on('step:pass', function (step) {
343 const allure = this.getAllure(step.cid)
344 logger('step:pass', step)
345
346 allure.endStep('passed')
347 })
348
349 this.on('step:fail', function (step) {
350 const allure = this.getAllure(step.cid)
351 logger('step:fail', step)
352
353 this.dumpJSON(allure, 'Verification Failures', step.errs)
354
355 if (step.screenshots) {
356 for (const screenshotMessage in step.screenshots) {
357 for (let i = 0; i < step.screenshots[screenshotMessage].length; ++i) {
358 allure.addAttachment(`${screenshotMessage} (${(i + 1)})`, new Buffer(fs.readFileSync(step.screenshots[screenshotMessage][i], {encoding: 'base64'}), 'base64'))
359 }
360 }
361 }
362
363 allure.endStep('failed')
364 })
365
366 this.on('step:broken', function (step) {
367 const allure = this.getAllure(step.cid)
368 logger('step:broken', step)
369
370 this.dumpJSON(allure, 'Errors', step.assertion)
371
372 if (step.assertion.screenshotFilename) {
373 allure.addAttachment(step.assertion.message, new Buffer(fs.readFileSync(step.assertion.screenshotFilename, {encoding: 'base64'}), 'base64'))
374 }
375
376 allure.endStep('broken')
377 })
378
379 this.on('step:unverified', function (step) {
380 const allure = this.getAllure(step.cid)
381 logger('step:unverified', step)
382
383 this.dumpJSON(allure, 'Unverified Specifications', step.err)
384
385 allure.endStep('unknown')
386 })
387
388 /*
389 attachment : {
390 cid: ...,
391 event: 'test:attach',
392 title: string,
393 file: string,
394 type: string
395 }
396
397 */
398 this.on('test:attach', function (attachment) {
399 const allure = this.getAllure(attachment.cid)
400 logger('test:attach', attachment)
401
402 if (this.isAnyTestRunning(allure)) {
403 error('ERROR', 'cannot attach because no test is running', attachment)
404 return
405 }
406 allure.addAttachment(
407 attachment.title,
408 fs.readFileSync(attachment.file),
409 attachment.type
410 )
411 })
412
413 /*
414 log : {
415 cid: ...,
416 event: 'test:log',
417 message: string,
418 detail: object
419 }
420
421 */
422 this.on('test:log', function (log) {
423 const allure = this.getAllure(log.cid)
424 logger('test:log', log)
425
426 if (this.isAnyTestRunning(allure)) {
427 error('ERROR', 'cannot log because no test is running', log)
428 return
429 }
430 const content = log.detail != null ? JSON.stringify(log.detail, null, ' ') : ''
431 allure.addAttachment(
432 log.message, content, 'application/json'
433 )
434 })
435
436 this.on('runner:screenshot', function (command) {
437 // const allure = this.getAllure(command.cid)
438 })
439
440 this.on('hook:start', (hook) => {
441 const allure = this.getAllure(hook.cid)
442
443 if (!allure.getCurrentSuite() || LOGGING_HOOKS.indexOf(hook.title) === -1) {
444 return
445 }
446
447 allure.startCase(hook.title)
448 })
449
450 this.on('hook:end', (hook) => {
451 const allure = this.getAllure(hook.cid)
452
453 if (!allure.getCurrentSuite() || LOGGING_HOOKS.indexOf(hook.title) === -1) {
454 return
455 }
456
457 allure.endCase('passed')
458
459 if (allure.getCurrentTest().steps.length === 0) {
460 allure.getCurrentSuite().testcases.pop()
461 }
462 })
463 }
464
465 handleMetadata (meta) {
466 var allure = this.getAllure(meta.cid)
467 logger('test:meta', meta)
468
469 if (!this.isAnyTestRunning(allure)) {
470 error('ERROR', 'test:meta : NO TEST RUNNING')
471 return
472 }
473 const currentTest = allure.getCurrentTest()
474
475 // manage description
476 if (meta.description) {
477 currentTest.setDescription(meta.description)
478 }
479 // manage labels ( feature, story, issue )
480 if (meta.feature) {
481 if (typeof (meta.feature) === 'string') {
482 currentTest.addLabel('feature', meta.feature)
483 } else {
484 for (const i in meta.feature) {
485 currentTest.addLabel('feature', meta.feature[i])
486 }
487 }
488 }
489 if (meta.story) {
490 if (typeof (meta.story) === 'string') {
491 currentTest.addLabel('story', meta.story)
492 } else {
493 for (const i in meta.story) {
494 currentTest.addLabel('story', meta.story[i])
495 }
496 }
497 }
498 if (meta.issue) {
499 if (typeof (meta.issue) === 'string') {
500 currentTest.addLabel('issue', meta.issue)
501 } else {
502 for (const issue of meta.issue) { // string array, previously "in"
503 currentTest.addLabel('issue', issue)
504 }
505 }
506 }
507 if (meta.bug) {
508 if (typeof (meta.bug) === 'string') {
509 currentTest.addLabel('issue', this.getBug(meta.bug)) // check if 'bug' can be used instead!
510 currentTest.addLabel('bug', this.getBug(meta.bug))
511 } else {
512 for (const bug of meta.bug) {
513 currentTest.addLabel('issue', this.getBug(bug))
514 currentTest.addLabel('bug', this.getBug(bug))
515 }
516 }
517 }
518 if (meta.severity) {
519 if (typeof (meta.severity) === 'string') {
520 currentTest.addLabel('severity', meta.severity)
521 } else {
522 error('ERROR : meta.severity should be a string', meta)
523 }
524 }
525 // manage parameters
526 if (meta.argument) {
527 if (typeof (meta.argument) === 'object') {
528 const keys = Object.keys(meta.argument)
529 for (const i in keys) {
530 const key = keys[i]
531 const val = meta.argument[key]
532 currentTest.addParameter('argument', key, val)
533 }
534 } else {
535 error('ERROR : meta.argument should be an object { name2: val1, name2: val2.. }', meta)
536 }
537 }
538 if (meta.environment) {
539 if (typeof (meta.environment) === 'object') {
540 const keys = Object.keys(meta.environment)
541 for (const i in keys) {
542 const key = keys[i]
543 const val = meta.environment[key]
544 currentTest.addParameter('environment-variable', key, val)
545 }
546 } else {
547 error('ERROR : meta.environment should be an object { name2: val1, name2: val2.. }', meta)
548 }
549 }
550 }
551
552 getBug (bug) {
553 if (this.config.allure && this.config.allure.bugAppendix) {
554 bug = `${bug}${this.config.allure.bugAppendix}`
555 }
556 if (this.config.allure && this.config.allure.bugPrefix) {
557 bug = `${this.config.allure.bugPrefix}${bug}`
558 }
559 return bug
560 }
561
562 // add test.arguments to current test
563 addArguments (test) {
564 if (test.arguments) {
565 for (const key in test.arguments) {
566 if (test.arguments[key].value) {
567 this.getAllure(test.cid).getCurrentTest().addParameter('argument', test.arguments[key].caption, test.arguments[key].value)
568 }
569 }
570 }
571 }
572
573 buildResultStr (results) {
574 let str = ''
575
576 str += `${results.passing.count} passing (~${Math.round(results.passing.percentage)}%)`
577
578 if (results.skipped.count > 0) {
579 str += `, ${results.skipped.count} skipped (~${Math.round(results.skipped.percentage)}%)`
580 }
581
582 if (results.unverified.count > 0) {
583 str += `, ${results.unverified.count} unverified (~${Math.round(results.unverified.percentage)}%)`
584 }
585
586 if (results.failing.count > 0) {
587 str += `, ${results.failing.count} failing (~${Math.round(results.failing.percentage)}%)`
588 }
589
590 if (results.broken.count > 0) {
591 str += `, ${results.broken.count} broken (~${Math.round(results.broken.percentage)}%)`
592 }
593
594 return str
595 }
596
597 addResultsDummy (test) {
598 if (test.cid) {
599 const allure = this.getAllure(test.cid)
600 const currentTest = allure.getCurrentTest()
601
602 this.testIds[currentTest] = true
603
604 currentTest.addParameter('environment-variable', 'TESTCASE RESULTS: ', 'TESTCASE_RESULTS_STR')
605 currentTest.addParameter('environment-variable', 'SPEC RESULTS: ', 'SPEC_RESULTS_STR')
606 }
607 }
608
609 addResults () {
610 const tcResults = this.baseReporter.getTestcaseResults()
611 const specResults = this.baseReporter.getSpecResults()
612
613 let tcResultsStr = this.buildResultStr(tcResults)
614 let specResultsStr = this.buildResultStr(specResults)
615
616 const latestRun = fs.readFileSync(this.config.latestRunPath, 'utf8')
617
618 // replace TESTCASE_RESULTS_STR and SPEC_RESULTS_STR in all xml and json files in results folder
619 // unfortunately there is not better way to do this at the moment
620 getAllFiles(path.join(this.config.resultsPath, latestRun), '.xml').forEach(
621 file => {
622 let content = fs.readFileSync(file, 'utf8')
623
624 content = content.replace(/TESTCASE_RESULTS_STR/g, tcResultsStr)
625 content = content.replace(/SPEC_RESULTS_STR/g, specResultsStr)
626
627 fs.unlinkSync(file)
628
629 fs.writeFileSync(file, content, 'utf8')
630 }
631 )
632 }
633
634 getAllure (cid) {
635 if (typeof cid === 'undefined') {
636 cid = process.workflo.cid
637 } else {
638 if (typeof process.workflo === 'undefined') {
639 process.workflo = {
640 }
641 } else {
642 process.workflo.currentCid = cid // REMOVE???
643 }
644 }
645
646 if (this.allures[cid]) {
647 return this.allures[cid]
648 }
649
650 const allure = new Allure()
651 allure.setOptions({ targetDir: this.options.outputDir || 'allure-results' })
652 this.allures[cid] = allure
653 return this.allures[cid]
654 }
655
656 isAnyTestRunning (allure) {
657 return allure.getCurrentSuite() && allure.getCurrentTest()
658 }
659
660 dumpJSON (allure, name, json) {
661 const jsonStr = JSON.stringify(json, null, ' ').split(`\\\"`).join(`'`).replace(/(?:\\n)/g, '')
662 allure.addAttachment(name, jsonStr, 'application/json')
663 }
664}
665
666const read = (dir) =>
667fs.readdirSync(dir)
668.reduce(
669 (files, file) =>
670 fs.statSync(path.join(dir, file)).isDirectory()
671 ? files.concat(read(path.join(dir, file)))
672 : files.concat(path.join(dir, file)),
673 []
674)
675
676function getAllFiles (dirPath, extension) {
677 return read(dirPath).filter(
678 (fileName) => fileName.endsWith(extension)
679 )
680}
681
682export default AllureReporter