/** * used to wrap mocha, jasmine test frameworks functions (`it`, `beforeEach` and other) * with WebdriverIO before/after Test/Hook hooks. * Entrypoint is `wrapGlobalTestMethod`, other functions are exported for testing purposes. * * NOTE: not used by cucumber test framework. `testFnWrapper` is called directly there */ import { filterSpecArgs } from '../utils.js' import { testFnWrapper } from './testFnWrapper.js' import type { HookFnArgs, SpecFunction, BeforeHookParam, AfterHookParam, SpecArguments } from './types.js' const MOCHA_COMMANDS: ['skip', 'only'] = ['skip', 'only'] /** * runs a hook and execute before/after hook * * @param {Function} hookFn function that was passed to the framework hook * @param {Function} origFn original framework hook function * @param {Function} beforeFn before hook * @param {Function} beforeFnArgs function that returns args for `beforeFn` * @param {Function} afterFn after hook * @param {Function} afterArgsFn function that returns args for `afterFn` * @param {string} cid cid * @param {number} repeatTest number of retries if hook fails * @return {Function} wrapped framework hook function */ export const runHook = function ( this: unknown, hookFn: Function, origFn: Function, beforeFn: Function | Function[], beforeFnArgs: HookFnArgs, afterFn: Function | Function[], afterFnArgs: HookFnArgs, cid: string, repeatTest: number, timeout: number ) { const wrappedHook = function ( this: unknown, ...hookFnArgs: [ string, SpecFunction, BeforeHookParam, AfterHookParam, string, number, string ] ) { return testFnWrapper.call( this, 'Hook', { specFn: hookFn, specFnArgs: filterSpecArgs(hookFnArgs) }, { beforeFn, beforeFnArgs }, { afterFn, afterFnArgs }, cid, repeatTest, origFn.name ) } /** * make sure Mocha grabs the correct hook function body */ wrappedHook.toString = () => hookFn.toString() return origFn(wrappedHook, timeout) } /** * runs a spec function (test function) * * @param {string} specTitle test description * @param {Function} specFn test function that got passed in from the user * @param {Function} origFn original framework test function * @param {Function} beforeFn before hook * @param {Function} beforeFnArgs function that returns args for `beforeFn` * @param {Function} afterFn after hook * @param {Function} afterFnArgs function that returns args for `afterFn` * @param {string} cid cid * @param {number} repeatTest number of retries if test fails * @return {Function} wrapped test function */ export const runSpec = function ( this: unknown, specTitle: string, specFn: Function, origFn: Function, beforeFn: Function | Function[], beforeFnArgs: HookFnArgs, afterFn: Function | Function[], afterFnArgs: HookFnArgs, cid: string, repeatTest: number, timeout: number ) { const wrappedFn = function ( this: unknown, ...specFnArgs: [ string, SpecFunction, BeforeHookParam, AfterHookParam, string, number ] ) { return testFnWrapper.call( this, 'Test', { specFn, specFnArgs: filterSpecArgs(specFnArgs) }, { beforeFn, beforeFnArgs }, { afterFn, afterFnArgs }, cid, repeatTest ) } /** * make sure Mocha grabs the correct test function body */ wrappedFn.toString = () => specFn.toString() return origFn(specTitle, wrappedFn, timeout) } /** * wraps hooks and test function of a framework within a fiber context * * @param {Function} origFn original framework function * @param {Boolean} isSpec whether or not origFn is a spec * @param {string[]} testInterfaceFnNames command that runs specs, e.g. `it`, `it.only` or `fit` * @param {Function} beforeFn before hook * @param {Function} beforeFnArgs function that returns args for `beforeFn` * @param {Function} afterFn after hook * @param {Function} afterArgsFn function that returns args for `afterFn` * @param {string} cid cid * @return {Function} wrapped test/hook function */ export const wrapTestFunction = function ( this: unknown, origFn: Function, isSpec: boolean, beforeFn: Function | Function[], beforeArgsFn: HookFnArgs, afterFn: Function | Function[], afterArgsFn: HookFnArgs, cid: string ) { return function (...specArguments: SpecArguments) { /** * Variadic arguments: * [title, fn], [title], [fn] * [title, fn, retryCnt], [title, retryCnt], [fn, retryCnt] */ let retryCnt = typeof specArguments[specArguments.length - 1] === 'number' ? specArguments.pop() : 0 /** * Jasmine uses a timeout value as last parameter, in this case the arguments * should be [title, fn, timeout, retryCnt] */ // @ts-expect-error let timeout = globalThis.jasmine?.DEFAULT_TIMEOUT_INTERVAL // @ts-expect-error if (globalThis.jasmine) { // if we have [title, fn, timeout, retryCnt] if (typeof specArguments[specArguments.length - 1] === 'number') { timeout = specArguments.pop() as number // if we have [title, fn, timeout] } else { timeout = retryCnt as number retryCnt = 0 } } const specFn = typeof specArguments[0] === 'function' ? specArguments.shift() : (typeof specArguments[1] === 'function' ? specArguments[1] : undefined) const specTitle = specArguments[0] if (isSpec) { if (specFn) { return runSpec( specTitle as string, specFn as Function, origFn, beforeFn, beforeArgsFn, afterFn, afterArgsFn, cid, retryCnt as number, timeout ) } /** * if specFn is undefined we are dealing with a pending function */ return origFn(specTitle) } return runHook( specFn as Function, origFn, beforeFn, beforeArgsFn, afterFn, afterArgsFn, cid, retryCnt as number, timeout ) } } /** * Wraps global test function like `it`. * * The scope parameter is used in the qunit framework since all functions are bound to global.QUnit instead of global * * @param {boolean} isTest is `origFn` test function, otherwise hook * @param {Function} beforeFn before hook * @param {Function} beforeFnArgs function that returns args for `beforeFn` * @param {Function} afterFn after hook * @param {Function} afterArgsFn function that returns args for `afterFn` * @param {string} fnName test interface command to wrap, e.g. `beforeEach` * @param {string} cid cid * @param {Object} scope the scope to run command from, defaults to global */ export const wrapGlobalTestMethod = function ( this: unknown, isSpec: boolean, beforeFn: Function | Function[], beforeArgsFn: HookFnArgs, afterFn: Function | Function[], afterArgsFn: HookFnArgs, fnName: string, cid: string, scope = globalThis ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const origFn = (scope as any)[fnName]; // eslint-disable-next-line @typescript-eslint/no-explicit-any (scope as any)[fnName] = wrapTestFunction( origFn, isSpec, beforeFn, beforeArgsFn, afterFn, afterArgsFn, cid ) // eslint-disable-next-line @typescript-eslint/no-explicit-any addMochaCommands(origFn, (scope as any)[fnName]) } type ItFn = { (): ItFn skip: Function only: Function } /** * support `it.skip` and `it.only` for the Mocha framework * @param {Function} origFn original function * @param {function} newFn wrapped function */ function addMochaCommands (origFn: ItFn, newFn: ItFn) { MOCHA_COMMANDS.forEach((commandName: 'skip' | 'only') => { if (typeof origFn[commandName] === 'function') { newFn[commandName] = origFn[commandName] } }) }