webdriverio/wdio-jasmine-framework

View on GitHub
lib/adapter.js

Summary

Maintainability
B
6 hrs
Test Coverage
import Jasmine from 'jasmine'
import JasmineReporter from './reporter'
import { runInFiberContext, wrapCommands, executeHooksWithArgs } from 'wdio-sync'

const INTERFACES = {
    bdd: ['beforeAll', 'beforeEach', 'it', 'xit', 'fit', 'afterEach', 'afterAll']
}

const DEFAULT_TIMEOUT_INTERVAL = 60000

/**
 * Jasmine 2.x runner
 */
class JasmineAdapter {
    constructor (cid, config, specs = [], capabilities = {}) {
        this.cid = cid
        this.capabilities = capabilities
        this.specs = specs
        this.config = Object.assign({}, config)
        this.jasmineNodeOpts = Object.assign({
            cleanStack: true
        }, config.jasmineNodeOpts)
        this.jrunner = {}
        this.reporter = new JasmineReporter({
            cid: this.cid,
            capabilities: this.capabilities,
            specs: this.specs,
            cleanStack: this.jasmineNodeOpts.cleanStack
        })
    }

    async run () {
        let self = this

        this.jrunner = new Jasmine()
        let jasmine = this.jrunner.jasmine

        this.jrunner.randomizeTests(this.getRandomExecutionPolicy())

        this.jrunner.projectBaseDir = ''
        this.jrunner.specDir = ''
        this.jrunner.addSpecFiles(this.specs)

        jasmine.DEFAULT_TIMEOUT_INTERVAL = this.getDefaultInterval()
        jasmine.getEnv().addReporter(this.reporter)

        /**
         * Filter specs to run based on jasmineNodeOpts.grep and jasmineNodeOpts.invert
         */
        jasmine.getEnv().specFilter = (spec) => {
            let grepMatch = this.getGrepMatch(spec)
            let invertGrep = !!this.jasmineNodeOpts.invertGrep
            if (grepMatch === invertGrep) {
                spec.pend()
            }
            return true
        }

        /**
         * Set whether to stop suite execution when a spec fails
         */
        const stopOnSpecFailure = !!this.jasmineNodeOpts.stopOnSpecFailure
        jasmine.getEnv().stopOnSpecFailure(stopOnSpecFailure)

        /**
         * enable expectHandler
         */
        jasmine.Spec.prototype.addExpectationResult = this.getExpectationResultHandler(jasmine)

        /**
         * patch jasmine to support promises
         */
        INTERFACES['bdd'].forEach((fnName) => {
            const origFn = global[fnName]
            global[fnName] = (...args) => {
                const retryCnt = typeof args[args.length - 1] === 'number' ? args.pop() : 0
                const specFn = typeof args[0] === 'function' ? args.shift() : (typeof args[1] === 'function' ? args.pop() : undefined)
                const specTitle = args[0]
                const patchedOrigFn = function (done) {
                    // specFn will be replaced by wdio-sync and will always return a promise
                    return specFn.call(this).then(() => done(), (e) => done.fail(e))
                }
                const newArgs = [specTitle, patchedOrigFn, retryCnt].filter(param => {
                    if (param === '') {
                        return true
                    } else {
                        return Boolean(param)
                    }
                })

                if (!specFn) {
                    return origFn(specTitle)
                }

                return origFn.apply(this, newArgs)
            }
        })

        /**
         * wrap commands with wdio-sync
         */
        wrapCommands(global.browser, this.config.beforeCommand, this.config.afterCommand)
        INTERFACES['bdd'].forEach((fnName) => runInFiberContext(
            ['it', 'fit'],
            this.config.beforeHook,
            this.config.afterHook,
            fnName
        ))

        /**
         * for a clean stdout we need to avoid that Jasmine initialises the
         * default reporter
         */
        Jasmine.prototype.configureDefaultReporter = () => {}

        /**
         * wrap Suite and Spec prototypes to get access to their data
         */
        let beforeAllMock = jasmine.Suite.prototype.beforeAll
        jasmine.Suite.prototype.beforeAll = function (...args) {
            self.lastSpec = this.result
            beforeAllMock.apply(this, args)
        }
        let executeMock = jasmine.Spec.prototype.execute
        jasmine.Spec.prototype.execute = function (...args) {
            self.lastTest = this.result
            self.lastTest.start = new Date().getTime()
            executeMock.apply(this, args)
        }

        await executeHooksWithArgs(this.config.before, [this.capabilities, this.specs])
        let result = await new Promise((resolve) => {
            this.jrunner.env.beforeAll(this.wrapHook('beforeSuite'))
            this.jrunner.env.beforeEach(this.wrapHook('beforeTest'))
            this.jrunner.env.afterEach(this.wrapHook('afterTest'))
            this.jrunner.env.afterAll(this.wrapHook('afterSuite'))

            this.jrunner.onComplete(() => resolve(this.reporter.getFailedCount()))
            this.jrunner.execute()
        })
        await executeHooksWithArgs(this.config.after, [result, this.capabilities, this.specs])
        await this.reporter.waitUntilSettled()
        return result
    }

    /**
     * Hooks which are added as true Mocha hooks need to call done() to notify async
     */
    wrapHook (hookName) {
        return (done) => executeHooksWithArgs(
            this.config[hookName],
            this.prepareMessage(hookName)
        ).then(() => done(), (e) => {
            console.log(`Error in ${hookName} hook: ${e.stack.slice(7)}`)
            done()
        })
    }

    prepareMessage (hookName) {
        const params = { type: hookName }

        switch (hookName) {
        case 'beforeSuite':
        case 'afterSuite':
            params.payload = Object.assign({
                file: this.jrunner.specFiles[0]
            }, this.lastSpec)
            break
        case 'beforeTest':
        case 'afterTest':
            params.payload = Object.assign({
                file: this.jrunner.specFiles[0]
            }, this.lastTest)
            break
        }

        return this.formatMessage(params)
    }

    formatMessage (params) {
        let message = {
            type: params.type
        }

        if (params.err) {
            message.err = {
                message: params.err.message,
                stack: params.err.stack
            }
        }

        if (params.payload) {
            message.title = params.payload.description
            message.fullName = params.payload.fullName || null
            message.file = params.payload.file

            if (params.payload.id && params.payload.id.startsWith('spec')) {
                message.parent = this.lastSpec.description
                message.passed = params.payload.failedExpectations.length === 0
            }

            if (params.type === 'afterTest') {
                message.duration = new Date().getTime() - params.payload.start
                message.passed = params.payload.failedExpectations.length === 0
            }

            if (typeof params.payload.duration === 'number') {
                message.duration = params.payload.duration
            }
        }

        return message
    }

    getDefaultInterval () {
        let { jasmineNodeOpts } = this
        if (jasmineNodeOpts.defaultTimeoutInterval) {
            return jasmineNodeOpts.defaultTimeoutInterval
        }

        return DEFAULT_TIMEOUT_INTERVAL
    }

    getRandomExecutionPolicy () {
        return !!this.jasmineNodeOpts.random
    }

    getGrepMatch (spec) {
        let { grep } = this.jasmineNodeOpts
        return !grep || spec.getFullName().match(new RegExp(grep)) !== null
    }

    getExpectationResultHandler (jasmine) {
        let { jasmineNodeOpts } = this
        const origHandler = jasmine.Spec.prototype.addExpectationResult

        if (typeof jasmineNodeOpts.expectationResultHandler !== 'function') {
            return origHandler
        }

        return this.expectationResultHandler(origHandler)
    }

    expectationResultHandler (origHandler) {
        let { expectationResultHandler } = this.jasmineNodeOpts
        return function (passed, data) {
            try {
                expectationResultHandler.call(global.browser, passed, data)
            } catch (e) {
                /**
                 * propagate expectationResultHandler error if actual assertion passed
                 */
                if (passed) {
                    passed = false
                    data = {
                        passed: false,
                        message: 'expectationResultHandlerError: ' + e.message
                    }
                }
            }

            return origHandler.call(this, passed, data)
        }
    }
}

const _JasmineAdapter = JasmineAdapter
const adapterFactory = {}

adapterFactory.run = async function (cid, config, specs, capabilities) {
    const adapter = new _JasmineAdapter(cid, config, specs, capabilities)
    const result = await adapter.run()
    return result
}

export default adapterFactory
export { JasmineAdapter, adapterFactory }