|
| 1 | +const { |
| 2 | + reporters: { Base }, |
| 3 | +} = require('mocha'); |
| 4 | +const ms = require('ms'); |
| 5 | +const event = require('../event'); |
| 6 | +const AssertionFailedError = require('../assert/error'); |
| 7 | +const output = require('../output'); |
| 8 | + |
| 9 | +const cursor = Base.cursor; |
| 10 | +let currentMetaStep = []; |
| 11 | +let codeceptjsEventDispatchersRegistered = false; |
| 12 | + |
| 13 | +class Cli extends Base { |
| 14 | + constructor(runner, opts) { |
| 15 | + super(runner); |
| 16 | + let level = 0; |
| 17 | + this.loadedTests = []; |
| 18 | + opts = opts.reporterOptions || opts; |
| 19 | + if (opts.steps) level = 1; |
| 20 | + if (opts.debug) level = 2; |
| 21 | + if (opts.verbose) level = 3; |
| 22 | + output.level(level); |
| 23 | + output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`); |
| 24 | + output.print(`Using test root "${global.codecept_dir}"`); |
| 25 | + |
| 26 | + const showSteps = level >= 1; |
| 27 | + |
| 28 | + if (level >= 2) { |
| 29 | + const Containter = require('../container'); |
| 30 | + output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)); |
| 31 | + output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)); |
| 32 | + } |
| 33 | + |
| 34 | + runner.on('start', () => { |
| 35 | + console.log(); |
| 36 | + }); |
| 37 | + |
| 38 | + runner.on('suite', suite => { |
| 39 | + output.suite.started(suite); |
| 40 | + }); |
| 41 | + |
| 42 | + runner.on('fail', test => { |
| 43 | + if (test.ctx.currentTest) { |
| 44 | + this.loadedTests.push(test.ctx.currentTest.uid); |
| 45 | + } |
| 46 | + if (showSteps && test.steps) { |
| 47 | + return output.scenario.failed(test); |
| 48 | + } |
| 49 | + cursor.CR(); |
| 50 | + output.test.failed(test); |
| 51 | + }); |
| 52 | + |
| 53 | + runner.on('pending', test => { |
| 54 | + if (test.parent && test.parent.pending) { |
| 55 | + const suite = test.parent; |
| 56 | + const skipInfo = suite.opts.skipInfo || {}; |
| 57 | + skipTestConfig(test, skipInfo.message); |
| 58 | + } else { |
| 59 | + skipTestConfig(test, null); |
| 60 | + } |
| 61 | + this.loadedTests.push(test.uid); |
| 62 | + cursor.CR(); |
| 63 | + output.test.skipped(test); |
| 64 | + }); |
| 65 | + |
| 66 | + runner.on('pass', test => { |
| 67 | + if (showSteps && test.steps) { |
| 68 | + return output.scenario.passed(test); |
| 69 | + } |
| 70 | + cursor.CR(); |
| 71 | + output.test.passed(test); |
| 72 | + }); |
| 73 | + |
| 74 | + if (showSteps) { |
| 75 | + runner.on('test', test => { |
| 76 | + currentMetaStep = []; |
| 77 | + if (test.steps) { |
| 78 | + output.test.started(test); |
| 79 | + } |
| 80 | + }); |
| 81 | + |
| 82 | + if (!codeceptjsEventDispatchersRegistered) { |
| 83 | + codeceptjsEventDispatchersRegistered = true; |
| 84 | + |
| 85 | + event.dispatcher.on(event.bddStep.started, step => { |
| 86 | + output.stepShift = 2; |
| 87 | + output.step(step); |
| 88 | + }); |
| 89 | + |
| 90 | + event.dispatcher.on(event.step.started, step => { |
| 91 | + let processingStep = step; |
| 92 | + const metaSteps = []; |
| 93 | + while (processingStep.metaStep) { |
| 94 | + metaSteps.unshift(processingStep.metaStep); |
| 95 | + processingStep = processingStep.metaStep; |
| 96 | + } |
| 97 | + const shift = metaSteps.length; |
| 98 | + |
| 99 | + for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) { |
| 100 | + if (currentMetaStep[i] !== metaSteps[i]) { |
| 101 | + output.stepShift = 3 + 2 * i; |
| 102 | + if (!metaSteps[i]) continue; |
| 103 | + // bdd steps are handled by bddStep.started |
| 104 | + if (metaSteps[i].isBDD()) continue; |
| 105 | + output.step(metaSteps[i]); |
| 106 | + } |
| 107 | + } |
| 108 | + currentMetaStep = metaSteps; |
| 109 | + output.stepShift = 3 + 2 * shift; |
| 110 | + if (step.helper.constructor.name !== 'ExpectHelper') { |
| 111 | + output.step(step); |
| 112 | + } |
| 113 | + }); |
| 114 | + |
| 115 | + event.dispatcher.on(event.step.finished, () => { |
| 116 | + output.stepShift = 0; |
| 117 | + }); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + runner.on('suite end', suite => { |
| 122 | + let skippedCount = 0; |
| 123 | + const grep = runner._grep; |
| 124 | + for (const test of suite.tests) { |
| 125 | + if (!test.state && !this.loadedTests.includes(test.uid)) { |
| 126 | + if (matchTest(grep, test.title)) { |
| 127 | + if (!test.opts) { |
| 128 | + test.opts = {}; |
| 129 | + } |
| 130 | + if (!test.opts.skipInfo) { |
| 131 | + test.opts.skipInfo = {}; |
| 132 | + } |
| 133 | + skipTestConfig(test, "Skipped due to failure in 'before' hook"); |
| 134 | + output.test.skipped(test); |
| 135 | + skippedCount += 1; |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + this.stats.pending += skippedCount; |
| 141 | + this.stats.tests += skippedCount; |
| 142 | + }); |
| 143 | + |
| 144 | + runner.on('end', this.result.bind(this)); |
| 145 | + } |
| 146 | + |
| 147 | + result() { |
| 148 | + const stats = this.stats; |
| 149 | + stats.failedHooks = 0; |
| 150 | + console.log(); |
| 151 | + |
| 152 | + // passes |
| 153 | + if (stats.failures) { |
| 154 | + output.print(output.styles.bold('-- FAILURES:')); |
| 155 | + } |
| 156 | + |
| 157 | + const failuresLog = []; |
| 158 | + |
| 159 | + // failures |
| 160 | + if (stats.failures) { |
| 161 | + // append step traces |
| 162 | + this.failures.map(test => { |
| 163 | + const err = test.err; |
| 164 | + |
| 165 | + let log = ''; |
| 166 | + |
| 167 | + if (err instanceof AssertionFailedError) { |
| 168 | + err.message = err.inspect(); |
| 169 | + } |
| 170 | + |
| 171 | + const steps = test.steps || (test.ctx && test.ctx.test.steps); |
| 172 | + |
| 173 | + if (steps && steps.length) { |
| 174 | + let scenarioTrace = ''; |
| 175 | + steps.reverse().forEach(step => { |
| 176 | + const line = `- ${step.toCode()} ${step.line()}`; |
| 177 | + // if (step.status === 'failed') line = '' + line; |
| 178 | + scenarioTrace += `\n${line}`; |
| 179 | + }); |
| 180 | + log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; |
| 181 | + } |
| 182 | + |
| 183 | + // display artifacts in debug mode |
| 184 | + if (test?.artifacts && Object.keys(test.artifacts).length) { |
| 185 | + log += `\n${output.styles.bold('Artifacts:')}`; |
| 186 | + for (const artifact of Object.keys(test.artifacts)) { |
| 187 | + log += `\n- ${artifact}: ${test.artifacts[artifact]}`; |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + try { |
| 192 | + let stack = err.stack ? err.stack.split('\n') : []; |
| 193 | + if (stack[0] && stack[0].includes(err.message)) { |
| 194 | + stack.shift(); |
| 195 | + } |
| 196 | + |
| 197 | + if (output.level() < 3) { |
| 198 | + stack = stack.slice(0, 3); |
| 199 | + } |
| 200 | + |
| 201 | + err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; |
| 202 | + |
| 203 | + // clone err object so stack trace adjustments won't affect test other reports |
| 204 | + test.err = err; |
| 205 | + return test; |
| 206 | + } catch (e) { |
| 207 | + throw Error(e); |
| 208 | + } |
| 209 | + }); |
| 210 | + |
| 211 | + const originalLog = Base.consoleLog; |
| 212 | + Base.consoleLog = (...data) => { |
| 213 | + failuresLog.push([...data]); |
| 214 | + originalLog(...data); |
| 215 | + }; |
| 216 | + Base.list(this.failures); |
| 217 | + Base.consoleLog = originalLog; |
| 218 | + console.log(); |
| 219 | + } |
| 220 | + |
| 221 | + this.failures.forEach(failure => { |
| 222 | + if (failure.constructor.name === 'Hook') { |
| 223 | + stats.failedHooks += 1; |
| 224 | + } |
| 225 | + }); |
| 226 | + event.emit(event.all.failures, { failuresLog, stats }); |
| 227 | + output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks); |
| 228 | + |
| 229 | + if (stats.failures && output.level() < 3) { |
| 230 | + output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); |
| 231 | + } |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +function matchTest(grep, test) { |
| 236 | + if (grep) { |
| 237 | + return grep.test(test); |
| 238 | + } |
| 239 | + return true; |
| 240 | +} |
| 241 | + |
| 242 | +function skipTestConfig(test, message) { |
| 243 | + if (!test.opts) { |
| 244 | + test.opts = {}; |
| 245 | + } |
| 246 | + if (!test.opts.skipInfo) { |
| 247 | + test.opts.skipInfo = {}; |
| 248 | + } |
| 249 | + test.opts.skipInfo.message = test.opts.skipInfo.message || message; |
| 250 | + test.opts.skipInfo.isFastSkipped = true; |
| 251 | + event.emit(event.test.skipped, test); |
| 252 | + test.state = 'skipped'; |
| 253 | +} |
| 254 | + |
| 255 | +module.exports = function (runner, opts) { |
| 256 | + return new Cli(runner, opts); |
| 257 | +}; |
0 commit comments