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!,
hookArgsFn,
isTest ? afterTest : afterHook,
hookArgsFn,
fnName,
cid
)
})
const { compilers = [] } = options
return requireExternalModules([...compilers])
}
export async function loadModule (name: string) {
try {
return await import(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!')
}
}