🏡 index : quine-global/webdriverio.git

import { wrapGlobalTestMethod } from '@wdio/utils'

import { INTERFACES, TEST_INTERFACES, MOCHA_TIMEOUT_MESSAGE } from './constants.js'
import type { FormattedMessage, FrameworkMessage, MochaOpts } from './types.js'

/**
* Extracts the mocha UI type following this convention:
*  - If the mochaOpts.ui provided doesn't contain a '-' then the full name
*      is taken as ui type (i.e. 'bdd','tdd','qunit')
*  - If it contains a '-' then it asumes we are providing a custom ui for
*      mocha. Then it extracts the text after the last '-' (ignoring .js if
*      provided) as the interface type. (i.e. strong-bdd in
*      https://github.com/strongloop/strong-mocha-interfaces)
*/
const MOCHA_UI_TYPE_EXTRACTOR = /^(?:.*-)?([^-.]+)(?:.js)?$/
const DEFAULT_INTERFACE_TYPE = 'bdd'

interface Payload {
    title: string
    parent?: Payload | null
    file?: string
    duration?: number
    body?: string
    context?: unknown
    state?: string
    pending?: boolean
    ctx?: Mocha.Context
}

export function formatMessage (params: FrameworkMessage) {
    const message: FormattedMessage = {
        type: params.type
    }
    const payload = params.payload as Payload | undefined

    const mochaAllHooksIfPresent = payload?.title?.match(/^"(before|after)( all| each)?" hook/)

    if (params.err) {
        /**
         * replace "Ensure the done() callback is being called in this test." with a more meaningful message
         */
        if (params.err && params.err.message && params.err.message.includes(MOCHA_TIMEOUT_MESSAGE)) {
            const testName = (payload?.parent?.title ? `${payload.parent.title} ${payload.title}` : payload?.title) || 'unknown test'
            const replacement = (
                `The execution in the test "${testName}" took too long. Try to reduce the run time or ` +
                'increase your timeout for test specs (https://webdriver.io/docs/timeouts).'
            )
            params.err.message = params.err.message.replace(MOCHA_TIMEOUT_MESSAGE, replacement)
            params.err.stack = params.err.stack.replace(MOCHA_TIMEOUT_MESSAGE, replacement)
        }

        message.error = {
            name: params.err.name,
            message: params.err.message,
            stack: params.err.stack,
            type: params.err.type || params.err.name,
            expected: params.err.expected,
            actual: params.err.actual
        }

        /**
         * hook failures are emitted as "test:fail"
         */
        if (mochaAllHooksIfPresent) {
            message.type = 'hook:end'
        }
    }

    if (payload) {
        message.title = payload.title
        message.parent = payload.parent ? payload.parent.title : undefined

        let fullTitle = message.title
        if (payload.parent) {
            let parent = payload.parent
            while (parent && parent.title) {
                fullTitle = parent.title + '.' + fullTitle
                parent = parent.parent as Payload
            }
        }

        message.fullTitle = fullTitle
        message.pending = payload.pending || false
        message.file = payload.file
        message.duration = payload.duration
        message.body = payload.body

        /**
         * Add the current test title to the payload for cases where it helps to
         * identify the test, e.g. when running inside a beforeEach hook
         */
        if (payload.ctx && payload.ctx.currentTest) {
            message.currentTest = payload.ctx.currentTest.title
        }

        if (params.type.match(/Test/)) {
            message.passed = (payload.state === 'passed')
        }

        if (payload.parent?.title && mochaAllHooksIfPresent) {
            const hookName = mochaAllHooksIfPresent[0]
            message.title = `${hookName} for ${payload.parent.title}`
        }

        if (payload.context) { message.context = payload.context }
    }

    return message
}

/**
 * require external modules
 * @param mods list of modules to load before the test starts
 * @param loader  function to import the module, optional and for testing purposes only
 */
export function requireExternalModules (mods: string[], loader = loadModule) {
    return mods.map((mod) => {
        if (!mod) {
            return Promise.resolve()
        }

        mod = mod.includes(':') ? mod.substring(mod.lastIndexOf(':') + 1) : mod

        if (mod.startsWith('./') && globalThis.process) {
            mod = `${globalThis.process.cwd()}/${mod.slice(2)}`
        }

        return loader(mod)
    })
}

type Hook = Function | Function[]
export function setupEnv (cid: string, options: MochaOpts, beforeTest: Hook, beforeHook: Hook, afterTest: Hook, afterHook: Hook) {
    const match = MOCHA_UI_TYPE_EXTRACTOR.exec(options.ui!) as unknown as [string, keyof typeof INTERFACES]
    const type: keyof typeof INTERFACES = (match && INTERFACES[match[1]] && match[1]) || DEFAULT_INTERFACE_TYPE

    const hookArgsFn = (context: Mocha.Context) => {
        return [{ ...context.test, parent: context.test?.parent?.title }, context]
    }

    INTERFACES[type].forEach((fnName: string) => {
        const isTest = TEST_INTERFACES[type].flatMap((testCommand: string) => [testCommand, testCommand + '.only']).includes(fnName)

        wrapGlobalTestMethod(
            isTest,
            isTest ? beforeTest! : beforeHook!,
            // @ts-ignore
            hookArgsFn,
            isTest ? afterTest : afterHook,
            hookArgsFn,
            fnName,
            cid
        )
    })

    const { compilers = [] } = options
    return requireExternalModules([...compilers])
}

export async function loadModule (name: string) {
    try {
        return await import(/* @vite-ignore */name)
    } catch {
        throw new Error(`Module ${name} can't get loaded. Are you sure you have installed it?\n` +
                        'Note: if you\'ve installed WebdriverIO globally you need to install ' +
                        'these external modules globally too!')
    }
}