codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
283 lines (253 loc) • 7.47 kB
JavaScript
import promiseRetry from 'promise-retry'
import event from '../event.js'
import recorder from '../recorder.js'
import assertThrown from '../assert/throws.js'
import { ucfirst, isAsyncFunction } from '../utils.js'
import { getInjectedArguments } from './inject.js'
import { fireHook } from './hooks.js'
const injectHook = function (inject, suite) {
try {
inject()
} catch (err) {
recorder.throw(err)
}
recorder.catch(err => {
suiteTestFailedHookError(suite, err)
throw err
})
return recorder.promise()
}
function suiteTestFailedHookError(suite, err, hookName) {
suite.eachTest(test => {
test.err = err
if (hookName) hookName = ucfirst(hookName)
event.emit(event.test.failed, test, err, hookName)
})
}
function makeDoneCallableOnce(done) {
let called = false
return function (err) {
if (called) {
return
}
called = true
return done(err)
}
}
/**
* Wraps test function, injects support objects from container,
* starts promise chain with recorder, performs before/after hooks
* through event system.
*/
export function test(test) {
const testFn = test.fn
if (!testFn) {
return test
}
test.timeout(0)
test.async = true
test.fn = function (done) {
const doneFn = makeDoneCallableOnce(done)
recorder.errHandler(err => {
recorder.session.start('teardown')
recorder.cleanAsyncErr()
if (test.throws) {
// check that test should actually fail
try {
assertThrown(err, test.throws)
event.emit(event.test.passed, test)
event.emit(event.test.finished, test)
recorder.add(doneFn)
return
} catch (newErr) {
err = newErr
}
}
test.err = err
event.emit(event.test.failed, test, err)
event.emit(event.test.finished, test)
recorder.add(() => doneFn(err))
})
event.emit(event.test.started, test)
getInjectedArguments(testFn, test)
.then(args => {
// Start recorder to ensure any steps added within test function are executed
recorder.startUnlessRunning()
// Execute test function
const result = testFn.call(test, args)
// Wait for all recorder steps to complete
if (result && result.then) {
return result.then(() => recorder.promise())
}
return recorder.promise()
})
.then(() => {
recorder.add('fire test.passed', () => {
event.emit(event.test.passed, test)
event.emit(event.test.finished, test)
})
recorder.add('finish test', doneFn)
})
.catch(err => {
recorder.throw(err)
})
.finally(() => {
recorder.catch()
})
}
return test
}
/**
* Injects arguments to function from controller
*/
export function injected(fn, suite, hookName) {
return function (done) {
const doneFn = makeDoneCallableOnce(done)
const errHandler = err => {
recorder.session.start('teardown')
recorder.cleanAsyncErr()
if (['before', 'beforeSuite'].includes(hookName)) {
suiteTestFailedHookError(suite, err, hookName)
}
if (hookName === 'after') {
suiteTestFailedHookError(suite, err, hookName)
suite.eachTest(test => {
event.emit(event.test.after, test)
})
}
if (hookName === 'afterSuite') {
suiteTestFailedHookError(suite, err, hookName)
event.emit(event.suite.after, suite)
}
recorder.add(() => doneFn(err))
}
recorder.errHandler(err => {
errHandler(err)
})
if (!fn) throw new Error('fn is not defined')
fireHook(event.hook.started, suite)
this.test.body = fn.toString()
if (!recorder.isRunning()) {
recorder.errHandler(err => {
errHandler(err)
})
}
const opts = suite.opts || {}
const retries = opts[`retry${ucfirst(hookName)}`] || 0
const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
promiseRetry(
async (retry, number) => {
try {
recorder.startUnlessRunning()
const injectedArgs = await getInjectedArguments(fn, null, suite)
await fn.call(this, { ...injectedArgs, suite, test: currentTest })
await recorder.promise().catch(err => retry(err))
} catch (err) {
retry(err)
} finally {
if (number < retries) {
recorder.stop()
recorder.start()
}
}
},
{ retries },
)
.then(() => {
recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite))
recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
recorder.add(`finish ${hookName} hook`, doneFn)
recorder.catch()
})
.catch(e => {
recorder.throw(e)
recorder.catch(e => {
const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr()
errHandler(err)
})
recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e))
recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
})
}
}
/**
* Starts promise chain, so helpers could enqueue their hooks
*/
export function setup(suite) {
return function (done) {
const doneFn = makeDoneCallableOnce(done)
recorder.startUnlessRunning()
import('./test.js')
.then(testModule => {
const { enhanceMochaTest } = testModule.default || testModule
event.emit(event.test.before, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
recorder.add(() => doneFn())
})
.catch(err => {
doneFn(err)
})
}
}
export function teardown(suite) {
return function (done) {
const doneFn = makeDoneCallableOnce(done)
recorder.startUnlessRunning()
import('./test.js')
.then(testModule => {
const { enhanceMochaTest } = testModule.default || testModule
event.emit(event.test.after, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
recorder.add(() => doneFn())
})
.catch(err => {
doneFn(err)
})
}
}
export function suiteSetup(suite) {
return function (done) {
const doneFn = makeDoneCallableOnce(done)
recorder.startUnlessRunning()
// Set up error handler for suite setup
recorder.errHandler(err => {
doneFn(err)
})
import('./suite.js')
.then(suiteModule => {
const { enhanceMochaSuite } = suiteModule.default || suiteModule
event.emit(event.suite.before, enhanceMochaSuite(suite))
recorder.add(() => doneFn())
})
.catch(err => {
doneFn(err)
})
}
}
export function suiteTeardown(suite) {
return function (done) {
const doneFn = makeDoneCallableOnce(done)
recorder.startUnlessRunning()
// Set up error handler for suite teardown
recorder.errHandler(err => {
doneFn(err)
})
import('./suite.js')
.then(suiteModule => {
const { enhanceMochaSuite } = suiteModule.default || suiteModule
event.emit(event.suite.after, enhanceMochaSuite(suite))
recorder.add(() => doneFn())
})
.catch(err => {
doneFn(err)
})
}
}
export { getInjectedArguments }
export default {
test,
injected,
setup,
teardown,
suiteSetup,
suiteTeardown,
getInjectedArguments,
}