webdriverio/wdio-allure-reporter

View on GitHub
lib/reporter.js

Summary

Maintainability
F
4 days
Test Coverage
import process from 'process'
import events from 'events'
import Allure from 'allure-js-commons'
import Step from 'allure-js-commons/beans/step'
import {getTestStatus, isEmpty, PASSED, BROKEN, FAILED} from './utils'

const LOGGING_HOOKS = ['"before all" hook', '"after all" hook']
const STEP_STATUSES = [FAILED, PASSED, BROKEN]

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

        this.baseReporter = baseReporter
        this.config = config
        this.isMultiremote = false
        this.options = options
        this.allures = {}

        const { epilogue } = this.baseReporter

        if (this.options.useCucumberStepReporter) {
            // Hook events
            this.on('hook:start', ::this.cucumberHookStart)
            this.on('hook:end', ::this.cucumberHookEnd)
            // Test framework events
            this.on('suite:start', ::this.cucumberSuiteStart)
            this.on('suite:end', ::this.cucumberSuiteEnd)
            this.on('test:start', ::this.cucumberTestStart)
            this.on('test:pass', ::this.cucumberTestPass)
            this.on('test:fail', ::this.cucumberTestFail)
            this.on('test:pending', ::this.cucumberTestPending)
        } else {
            // Hook events
            this.on('hook:start', ::this.hookStart)
            this.on('hook:end', ::this.hookEnd)
            // Test framework events
            this.on('suite:start', ::this.suiteStart)
            this.on('suite:end', ::this.suiteEnd)
            this.on('test:start', ::this.testStart)
            this.on('test:pass', ::this.testPass)
            this.on('test:fail', ::this.testFail)
            this.on('test:pending', ::this.testPending)
        }

        // Runner events (webdriver)
        this.on('start', ::this.start)
        this.on('runner:command', ::this.runnerCommand)
        this.on('runner:step', ::this.step)
        this.on('runner:result', ::this.runnerResult)
        this.on('end', () => {
            epilogue.call(baseReporter)
        })

        // Allure events
        this.on('allure:feature', ::this.allureFeature)
        this.on('allure:addEnvironment', ::this.allureAddEnvironment)
        this.on('allure:addDescription', ::this.allureAddDescription)
        this.on('allure:attachment', ::this.allureAttachment)
        this.on('allure:story', ::this.allureStory)
        this.on('allure:severity', ::this.allureSeverity)
        this.on('allure:issue', ::this.allureIssue)
        this.on('allure:testId', ::this.allureTestId)
        this.on('allure:addArgument', ::this.allureAddArgument)
    }

    suiteStart (suite) {
        const allure = this.getAllure(suite.cid)
        const currentSuite = allure.getCurrentSuite()
        const prefix = currentSuite ? currentSuite.name + ' ' : ''
        allure.startSuite(prefix + suite.title)
    }

    suiteEnd (suite) {
        this.getAllure(suite.cid).endSuite()
    }

    testStart (test) {
        const allure = this.getAllure(test.cid)
        allure.startCase(test.title)
        const currentTest = allure.getCurrentTest()

        this.addBrowserOrDeviceParameter({ cid: test.cid, runner: test.runner })
        currentTest.addParameter('environment-variable', 'capabilities', JSON.stringify(test.runner[test.cid]))
        currentTest.addParameter('environment-variable', 'spec files', JSON.stringify(test.specs))

        if (test.featureName && test.scenarioName) {
            currentTest.addLabel('feature', test.featureName)
            currentTest.addLabel('story', test.scenarioName)
        }

        // Analytics labels More: https://github.com/allure-framework/allure2/blob/master/Analytics.md
        currentTest.addLabel('language', 'javascript')
        currentTest.addLabel('framework', 'wdio')
        currentTest.addLabel('thread', test.cid)
    }

    testPass (test) {
        this.getAllure(test.cid).endCase(PASSED)
    }

    testFail (test) {
        const allure = this.getAllure(test.cid)

        if (!allure.getCurrentTest()) {
            allure.startCase(test.title)
        } else {
            allure.getCurrentTest().name = test.title
        }

        const status = getTestStatus(test, this.config)
        while (allure.getCurrentSuite().currentStep instanceof Step) {
            allure.endStep(status)
        }

        allure.endCase(status, test.err)
    }

    testPending (test) {
        const allure = this.getAllure(test.cid)
        if (allure.getCurrentTest() && allure.getCurrentTest().status !== 'pending') {
            allure.endCase('pending')
        } else {
            allure.pendingCase(test.title)
        }
    }

    runnerCommand (command) {
        if (this.options.disableWebdriverStepsReporting || this.isMultiremote) {
            return
        }

        const allure = this.getAllure(command.cid)

        if (!this.isAnyTestRunning(allure)) {
            return
        }

        allure.startStep(`${command.method} ${command.uri.path}`)

        if (!isEmpty(command.data)) {
            this.dumpJSON(allure, 'Request', command.data)
        }
    }

    step ({event, cid, step}) {
        const allure = this.getAllure(cid)

        if (!this.isAnyTestRunning(allure)) {
            return
        }

        allure.startStep(step.title)

        if (!isEmpty(step.body)) {
            allure.addAttachment(step.bodyLabel, step.body)
        }

        if (STEP_STATUSES.indexOf(step.status) === -1) {
            throw new Error(`Step status must be ${STEP_STATUSES.join(' or ')}. You tried to set "${step.status}"`)
        }

        allure.endStep(step.status)
    }

    start (event) {
        this.isMultiremote = event.isMultiremote
    }

    runnerResult (command) {
        if (this.isMultiremote) {
            return
        }

        const allure = this.getAllure(command.cid)

        if (!this.isAnyTestRunning(allure)) {
            return
        }

        if (command.requestOptions.uri.path.match(/\/session\/[^/]*\/screenshot/) && command.body.value) {
            if (!this.options.disableWebdriverScreenshotsReporting) {
                allure.addAttachment('Screenshot', Buffer.from(command.body.value, 'base64'))
            }
        } else if (!this.options.disableWebdriverStepsReporting) {
            if (command.body) {
                this.dumpJSON(allure, 'Response', command.body)
            }

            allure.endStep(PASSED)
        }
    }

    hookStart (hook) {
        const allure = this.getAllure(hook.cid)

        if (!allure.getCurrentSuite() || LOGGING_HOOKS.indexOf(hook.title) === -1) {
            return
        }

        allure.startCase(hook.title)
    }

    hookEnd (hook) {
        const allure = this.getAllure(hook.cid)

        if (!allure.getCurrentSuite() || LOGGING_HOOKS.indexOf(hook.title) === -1) {
            return
        }

        allure.endCase(PASSED)

        if (allure.getCurrentTest().steps.length === 0) {
            allure.getCurrentSuite().testcases.pop()
        }
    }

    cucumberHookStart (hook) {
        const allure = this.getAllure(hook.cid)

        if (hook.title) {
            allure.startStep(hook.title)
        }
    }

    cucumberHookEnd (hook) {
        const allure = this.getAllure(hook.cid)

        if (hook.title) {
            allure.endStep(hook.title)
        }
        allure.endCase(PASSED)
    }

    cucumberSuiteStart (suite) {
        const allure = this.getAllure(suite.cid)

        if (!suite.parent) {
            const currentSuite = allure.getCurrentSuite()
            const prefix = currentSuite ? `${currentSuite.name} ` : ''
            allure.startSuite(prefix + suite.title)
        } else {
            if (suite.title) {
                allure.startCase(suite.title)
                const currentSuite = allure.getCurrentSuite()
                const test = allure.getCurrentTest()

                this.addBrowserOrDeviceParameter({ cid: suite.cid, runner: suite.runner })
                test.addLabel('feature', currentSuite.name)

                // Analytics labels More: https://github.com/allure-framework/allure2/blob/master/Analytics.md
                test.addLabel('language', 'javascript')
                test.addLabel('framework', 'wdio')
                test.addLabel('thread', suite.cid)
            }
        }
    }

    cucumberSuiteEnd (suite) {
        if (!suite.parent) {
            this.getAllure(suite.cid).endSuite()
        } else {
            if (suite.title) {
                this.getAllure(suite.cid).endCase(PASSED)
            }
        }
    }

    cucumberTestStart (test) {
        const allure = this.getAllure(test.cid)

        if (!this.isAnyTestRunning(allure)) {
            return
        }

        if (test.title) {
            allure.startStep(test.title)
        }
    }

    cucumberTestPass (test) {
        const allure = this.getAllure(test.cid)
        while (allure.getCurrentSuite().currentStep instanceof Step) {
            allure.endStep(PASSED)
        }
    }

    cucumberTestFail (test) {
        if (test.title) {
            const allure = this.getAllure(test.cid)
            if (!this.isAnyTestRunning(allure)) {
                return
            }

            const status = getTestStatus(test, this.config)

            while (allure.getCurrentSuite().currentStep instanceof Step) {
                allure.endStep(status)
            }

            allure.endCase(status, test.err)
        }
    }

    cucumberTestPending (test) {
        const allure = this.getAllure(test.cid)

        if (test.title) {
            allure.endStep('pending')
        }

        if (allure.getCurrentSuite() && allure.getCurrentSuite().status !== 'pending') {
            allure.endCase('pending')
        } else {
            allure.pendingCase(test.title)
        }
    }

    allureFeature ({ cid, featureName }) {
        const allure = this.getAllure(cid)
        const test = allure.getCurrentTest()
        test.addLabel('feature', featureName)
    }

    allureAddEnvironment ({ cid, name, value }) {
        const allure = this.getAllure(cid)
        const test = allure.getCurrentTest()
        test.addParameter('environment-variable', name, value)
    }

    allureAddDescription ({ cid, description, type }) {
        const allure = this.getAllure(cid)
        const test = allure.getCurrentTest()
        test.setDescription(description, type)
    }

    allureAttachment ({cid, name, content, type}) {
        const allure = this.getAllure(cid)
        if (!content || !allure) {
            return
        }

        if (type === 'application/json') {
            this.dumpJSON(allure, name, content)
        } else {
            allure.addAttachment(name, Buffer.from(content), type)
        }
    }

    allureStory ({ cid, storyName }) {
        const test = this.getAllure(cid).getCurrentTest()
        test.addLabel('story', storyName)
    }

    allureSeverity ({ cid, severity }) {
        const test = this.getAllure(cid).getCurrentTest()
        test.addLabel('severity', severity)
    }

    allureIssue ({ cid, issue }) {
        const test = this.getAllure(cid).getCurrentTest()
        test.addLabel('issue', issue)
    }

    allureTestId ({ cid, testId }) {
        const test = this.getAllure(cid).getCurrentTest()
        test.addLabel('testId', testId)
    }

    addBrowserOrDeviceParameter ({ cid, runner }) {
        const test = this.getAllure(cid).getCurrentTest()
        const { browserName, deviceName } = runner[cid]
        const targetName = browserName || deviceName || cid
        const version = runner[cid].version || runner[cid].platformVersion || ''
        const paramName = deviceName ? 'device' : 'browser'
        const paramValue = version ? `${targetName}-${version}` : targetName
        test.addParameter('argument', paramName, paramValue)
    }

    allureAddArgument ({ cid, name, value }) {
        const test = this.getAllure(cid).getCurrentTest()
        test.addParameter('argument', name, value)
    }

    static createStep (title, body, bodyLabel = 'attachment', status = PASSED) {
        const step = {
            title,
            body,
            bodyLabel,
            status
        }
        AllureReporter.tellReporter('runner:step', { step })
    }

    static feature (featureName) {
        AllureReporter.tellReporter('allure:feature', { featureName })
    }

    static addEnvironment (name, value) {
        AllureReporter.tellReporter('allure:addEnvironment', { name, value })
    }

    static addDescription (description, type) {
        AllureReporter.tellReporter('allure:addDescription', { description, type })
    }

    static createAttachment (name, content, type = 'text/plain') {
        AllureReporter.tellReporter('allure:attachment', {name, content, type})
    }

    static story (storyName) {
        AllureReporter.tellReporter('allure:story', { storyName })
    }

    static severity (severity) {
        AllureReporter.tellReporter('allure:severity', { severity })
    }

    static issue (issue) {
        AllureReporter.tellReporter('allure:issue', { issue })
    }

    static testId (testId) {
        AllureReporter.tellReporter('allure:testId', { testId })
    }

    static addArgument (name, value) {
        AllureReporter.tellReporter('allure:addArgument', { name, value })
    }

    static tellReporter (event, msg = {}) {
        process.send({ event, ...msg })
    }

    getAllure (cid) {
        if (this.allures[cid]) {
            return this.allures[cid]
        }

        const allure = new Allure()
        allure.setOptions({ targetDir: this.options.outputDir || 'allure-results' })
        this.allures[cid] = allure
        return this.allures[cid]
    }

    isAnyTestRunning (allure) {
        return allure.getCurrentSuite() && allure.getCurrentTest()
    }

    dumpJSON (allure, name, json) {
        allure.addAttachment(name, JSON.stringify(json, null, '    '), 'application/json')
    }
}

export default AllureReporter