webdriverio/wdio-spec-reporter

View on GitHub
lib/reporter.js

Summary

Maintainability
D
1 day
Test Coverage
import events from 'events'
import chalk from 'chalk'
import humanizeDuration from 'humanize-duration'

const DURATION_OPTIONS = {
    units: ['m', 's'],
    round: true,
    spacer: ''
}

/**
 * Initialize a new `spec` test reporter.
 *
 * @param {Runner} runner
 * @api public
 */
class SpecReporter extends events.EventEmitter {
    constructor (baseReporter, config, options = {}) {
        super()

        this.chalk = chalk
        this.baseReporter = baseReporter
        this.config = config
        this.options = options
        this.shortEnglishHumanizer = humanizeDuration.humanizer({
            language: 'shortEn',
            languages: { shortEn: {
                h: () => 'h',
                m: () => 'm',
                s: () => 's',
                ms: () => 'ms'
            }}
        })

        this.errorCount = 0
        this.indents = {}
        this.suiteIndents = {}
        this.specs = {}
        this.results = {}

        this.on('runner:start', function (runner) {
            this.suiteIndents[runner.cid] = {}
            this.indents[runner.cid] = 0
            this.specs[runner.cid] = runner.specs
            this.results[runner.cid] = {
                passing: 0,
                pending: 0,
                failing: 0
            }
        })

        this.on('suite:start', function (suite) {
            this.suiteIndents[suite.cid][suite.uid] = ++this.indents[suite.cid]
        })

        this.on('test:pending', function (test) {
            this.results[test.cid].pending++
        })

        this.on('test:pass', function (test) {
            this.results[test.cid].passing++
        })

        this.on('test:fail', function (test) {
            this.results[test.cid].failing++
        })

        this.on('suite:end', function (suite) {
            this.indents[suite.cid]--
        })

        this.on('runner:end', function (runner) {
            this.printSuiteResult(runner)
        })

        this.on('end', function () {
            this.printSuitesSummary()
        })
    }

    indent (cid, uid) {
        const indents = this.suiteIndents[cid][uid]
        return indents === 0 ? '' : Array(indents).join('    ')
    }

    getSymbol (state) {
        const { symbols } = this.baseReporter
        let symbol = '?' // in case of an unknown state

        switch (state) {
        case 'pass':
            symbol = symbols.ok
            break
        case 'pending':
            symbol = '-'
            break
        case 'fail':
            this.errorCount++
            symbol = this.errorCount + ')'
            break
        }

        return symbol
    }

    getColor (state) {
        let color = null // in case of an unknown state

        switch (state) {
        case 'pass':
        case 'passing':
            color = 'green'
            break
        case 'pending':
            color = 'cyan'
            break
        case 'fail':
        case 'failing':
            color = 'red'
            break
        }

        return color
    }

    getBrowserCombo (caps, verbose = true) {
        const device = caps.deviceName
        const browser = caps.browserName || caps.browser
        const version = caps.version || caps.platformVersion || caps.browser_version
        const platform = caps.os ? (caps.os + ' ' + caps.os_version) : (caps.platform || caps.platformName)

        /**
         * mobile capabilities
         */
        if (device) {
            const program = (caps.app || '').replace('sauce-storage:', '') || caps.browserName
            const executing = program ? `executing ${program}` : ''

            if (!verbose) {
                return `${device} ${platform} ${version}`
            }

            return `${device} on ${platform} ${version} ${executing}`.trim()
        }

        if (!verbose) {
            return (browser + ' ' + (version || '') + ' ' + (platform || '')).trim()
        }

        return browser + (version ? ` (v${version})` : '') + (platform ? ` on ${platform}` : '')
    }

    getResultList (cid, suites, preface = '') {
        let output = ''

        for (const specUid in suites) {
            // Remove "before all" tests from the displayed results
            if (specUid.indexOf('"before all"') === 0) {
                continue
            }

            const spec = suites[specUid]
            const indent = this.indent(cid, specUid)
            const specTitle = suites[specUid].title

            if (specUid.indexOf('"before all"') !== 0) {
                output += `${preface} ${indent}${specTitle}\n`
            }

            for (const testUid in spec.tests) {
                const test = spec.tests[testUid]
                const testTitle = spec.tests[testUid].title

                if (test.state === '') {
                    continue
                }

                output += preface
                output += '   ' + indent
                output += this.chalk[this.getColor(test.state)](this.getSymbol(test.state))
                output += ' ' + testTitle + '\n'
            }

            output += preface.trim() + '\n'
        }

        return output
    }

    getSummary (states, duration, preface = '') {
        let output = ''
        let displayedDuration = false

        for (const state in states) {
            const testCount = states[state]
            let testDuration = ''

            /**
             * don't display 0 passing/pending of failing test label
             */
            if (testCount === 0) {
                continue
            }

            /**
             * set duration
             */
            if (!displayedDuration) {
                testDuration = ' (' + this.shortEnglishHumanizer(duration, DURATION_OPTIONS) + ')'
            }

            output += preface + ' '
            output += this.chalk[this.getColor(state)](testCount)
            output += ' ' + this.chalk[this.getColor(state)](state)
            output += testDuration
            output += '\n'
            displayedDuration = true
        }

        return output
    }

    getPassesList (passes, preface) {
        let output = ''
        passes.forEach((test, i) => {
            const title = typeof test.parent !== 'undefined' ? test.parent + ' ' + test.title : test.title
            output += `${preface.trim()}\n`
            output += `${preface} ${(i + 1)}) ${title}:\n`
            if (test.nonerr) {
                for (var passItem of test.nonerr) {
                    output += `${preface} ${this.chalk.green(passItem.message)}\n`
                }
            }
        })
        return output
    }

    getFailureList (failures, preface) {
        let output = ''

        failures.forEach((test, i) => {
            const title = typeof test.parent !== 'undefined' ? test.parent + ' ' + test.title : test.title
            output += `${preface.trim()}\n`
            output += `${preface} ${(i + 1)}) ${title}:\n`
            for (var errItem of test.err) {
                output += `${preface} ${this.chalk.red(errItem.message)}\n`
                if (errItem.stack) {
                    const stack = errItem.stack.split(/\n/g).map((l) => `${preface} ${this.chalk.gray(l)}`).join('\n')
                    output += `${stack}\n`
                } else {
                    output += `${preface} ${this.chalk.gray('no stack available')}\n`
                }
            }
            if (test.nonerr) {
                for (var passItem of test.nonerr) {
                    output += `${preface} ${this.chalk.green(passItem.message)}\n`
                }
            }
        })

        return output
    }

    getJobLink (results, preface) {
        if (!results.config.host) {
            return ''
        }

        let output = ''
        if (results.config.host.indexOf('saucelabs.com') > -1 || results.config.sauceConnect === true) {
            output += `${preface.trim()}\n`
            output += `${preface} Check out job at https://saucelabs.com/tests/${results.sessionID}\n`
            return output
        }

        return output
    }

    getSuiteResult (runner) {
        const cid = runner.cid
        const stats = this.baseReporter.stats
        const results = stats.runners[cid]
        const preface = `[${this.getBrowserCombo(results.capabilities, false)} #${cid}]`
        const specHash = stats.getSpecHash(runner)
        const spec = results.specs[specHash]
        const combo = this.getBrowserCombo(results.capabilities)
        const failures = stats.getFailures().filter((f) => f.cid === cid || Object.keys(f.runner).indexOf(cid) > -1)
        const passes = stats.getPasses().filter((p) => p.cid === cid || Object.keys(p.runner).indexOf(cid) > -1)

        /**
         * don't print anything if no specs where executed
         */
        if (Object.keys(spec.suites).length === 0) {
            return ''
        }

        this.errorCount = 0
        let output = ''

        output += '------------------------------------------------------------------\n'

        /**
         * won't be available when running multiremote tests
         */
        if (results.sessionID) {
            output += `${preface} Session ID: ${results.sessionID}\n`
        }

        output += `${preface} Spec: ${this.specs[cid]}\n`

        /**
         * won't be available when running multiremote tests
         */
        if (combo) {
            output += `${preface} Running: ${combo}\n`
        }

        output += `${preface}\n`
        output += this.getResultList(cid, spec.suites, preface)
        output += `${preface}\n`
        output += this.getSummary(this.results[cid], spec._duration, preface)
        output += this.getPassesList(passes, preface)
        output += this.getFailureList(failures, preface)
        output += this.getJobLink(results, preface)
        output += `${preface}\n`
        return output
    }

    printSuiteResult (runner) {
        console.log(this.getSuiteResult(runner))
    }

    getSuitesSummary (specCount) {
        let output = '\n\n==================================================================\n'
        output += 'Number of specs: ' + specCount
        return output
    }

    printSuitesSummary () {
        const specCount = Object.keys(this.baseReporter.stats.runners).length

        /**
         * no need to print summary if only one runner was executed
         */
        if (specCount === 1) {
            return
        }

        const epilogue = this.baseReporter.epilogue
        console.log(this.getSuitesSummary(specCount))
        epilogue.call(this.baseReporter)
    }
}

export default SpecReporter