UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

327 lines (302 loc) 10.3 kB
import recorder from './recorder.js' import output from './output.js' import store from './store.js' import event from './event.js' import container from './container.js' import MetaStep from './step/meta.js' import { empty } from './assert/empty.js' import { isAsyncFunction } from './utils.js' /** * @param {CodeceptJS.LocatorOrString} context * @param {Function} fn * @return {Promise<*> | undefined} */ function within(context, fn) { const helpers = store.dryRun ? {} : container.helpers() const locator = typeof context === 'object' ? JSON.stringify(context) : context return recorder.add( 'register within wrapper', () => { const metaStep = new WithinStep(locator, fn) const defineMetaStep = step => (step.metaStep = metaStep) recorder.session.start('within') event.dispatcher.prependListener(event.step.before, defineMetaStep) Object.keys(helpers).forEach(helper => { if (helpers[helper]._withinBegin) recorder.add(`[${helper}] start within`, () => helpers[helper]._withinBegin(context)) }) const finalize = () => { event.dispatcher.removeListener(event.step.before, defineMetaStep) recorder.add('Finalize session within session', () => { output.stepShift = 1 recorder.session.restore('within') }) } const finishHelpers = () => { Object.keys(helpers).forEach(helper => { if (helpers[helper]._withinEnd) recorder.add(`[${helper}] finish within`, () => helpers[helper]._withinEnd()) }) } if (isAsyncFunction(fn)) { return fn() .then(res => { finishHelpers() finalize() return recorder.promise().then(() => res) }) .catch(e => { finishHelpers() finalize() recorder.throw(e) return recorder.promise() }) } let res try { res = fn() } catch (err) { recorder.throw(err) } finally { finishHelpers() recorder.catch(err => { output.stepShift = 1 throw err }) } finalize() return recorder.promise().then(() => res) }, false, false, ) } class WithinStep extends MetaStep { constructor(locator, fn) { super('Within') this.args = [locator] } toString() { return `${this.prefix}Within ${this.humanizeArgs()}${this.suffix}` } } /** * A utility function for CodeceptJS tests that acts as a soft assertion. * Executes a callback within a recorded session, ensuring errors are handled gracefully without failing the test immediately. * * @async * @function hopeThat * @param {Function} callback - The callback function containing the logic to validate. * This function should perform the desired assertion or condition check. * @returns {Promise<boolean|any>} A promise resolving to `true` if the assertion or condition was successful, * or `false` if an error occurred. * * @description * - Designed for use in CodeceptJS tests as a "soft assertion." * Unlike standard assertions, it does not stop the test execution on failure. * - Starts a new recorder session named 'hopeThat' and manages state restoration. * - Logs errors and attaches them as notes to the test, enabling post-test reporting of soft assertion failures. * - Resets the `store.hopeThat` flag after the execution, ensuring clean state for subsequent operations. * * @example * const { hopeThat } = require('codeceptjs/effects') * await hopeThat(() => { * I.see('Welcome'); // Perform a soft assertion * }); * * @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test. */ let hopeThatFailures = [] event.dispatcher.on(event.test.before, () => { hopeThatFailures = [] }) async function hopeThat(callback) { if (store.dryRun) return const sessionName = 'hopeThat' let result = false return recorder.add( 'hopeThat', () => { recorder.session.start(sessionName) store.hopeThat = true callback() recorder.add(() => { result = true recorder.session.restore(sessionName) return result }) recorder.session.catch(err => { result = false const msg = err.inspect ? err.inspect() : err.toString() output.debug(`Unsuccessful assertion > ${msg}`) hopeThatFailures.push(msg) event.dispatcher.once(event.test.finished, test => { if (!test.notes) test.notes = [] test.notes.push({ type: 'conditionalError', text: msg }) }) recorder.session.restore(sessionName) return result }) return recorder.add( 'result', () => { store.hopeThat = undefined return result }, true, false, ) }, false, false, ) } /** * Asserts that no `hopeThat` soft assertion has failed in the current test. * Call once at the end of a scenario to fail it when any soft assertion failed. */ hopeThat.noErrors = function () { const failures = hopeThatFailures hopeThatFailures = [] empty('soft assertions').assert(failures) } /** * A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval. * * @async * @function retryTo * @param {Function} callback - The function to execute, which will be retried upon failure. * Receives the current retry count as an argument. * @param {number} maxTries - The maximum number of attempts to retry the callback. * @param {number} [pollInterval=200] - The delay (in milliseconds) between retry attempts. * @returns {Promise<void|any>} A promise that resolves when the callback executes successfully, or rejects after reaching the maximum retries. * * @description * - This function is designed for use in CodeceptJS tests to handle intermittent or flaky test steps. * - Starts a new recorder session for each retry attempt, ensuring proper state management and error handling. * - Logs errors and retries the callback until it either succeeds or the maximum number of attempts is reached. * - Restores the session state after each attempt, whether successful or not. * * @example * const { retryTo } = require('codeceptjs/effects') * await retryTo((tries) => { * if (tries < 3) { * I.see('Non-existent element'); // Simulates a failure * } else { * I.see('Welcome'); // Succeeds on the 3rd attempt * } * }, 5, 300); // Retry up to 5 times, with a 300ms interval * * @throws Will reject with the last error encountered if the maximum retries are exceeded. */ async function retryTo(callback, maxTries, pollInterval = 200) { const sessionName = 'retryTo' return new Promise((done, reject) => { let tries = 0 function handleRetryException(err) { recorder.throw(err) reject(err) } const tryBlock = async () => { tries++ recorder.session.start(`${sessionName} ${tries}`) try { await callback(tries) } catch (err) { recorder.throw(err) } // Call done if no errors recorder.add(() => { recorder.session.restore(`${sessionName} ${tries}`) done(null) }) // Catch errors and retry recorder.session.catch(err => { recorder.session.restore(`${sessionName} ${tries}`) if (tries < maxTries) { output.debug(`Error ${err}... Retrying`) recorder.add(`${sessionName} ${tries}`, () => setTimeout(tryBlock, pollInterval)) } else { // if maxTries reached handleRetryException(err) } }) } recorder.add(sessionName, tryBlock).catch(err => { console.error('An error occurred:', err) done(null) }) }) } /** * A CodeceptJS utility function to attempt a step or callback without failing the test. * If the step fails, the test continues execution without interruption, and the result is logged. * * @async * @function tryTo * @param {Function} callback - The function to execute, which may succeed or fail. * This function contains the logic to be attempted. * @returns {Promise<boolean|any>} A promise resolving to `true` if the step succeeds, or `false` if it fails. * * @description * - Useful for scenarios where certain steps are optional or their failure should not interrupt the test flow. * - Starts a new recorder session named 'tryTo' for isolation and error handling. * - Captures errors during execution and logs them for debugging purposes. * - Ensures the `store.tryTo` flag is reset after execution to maintain a clean state. * * @example * const { tryTo } = require('codeceptjs/effects') * const wasSuccessful = await tryTo(() => { * I.see('Welcome'); // Attempt to find an element on the page * }); * * if (!wasSuccessful) { * I.say('Optional step failed, but test continues.'); * } * * @throws Will handle errors internally, logging them and returning `false` as the result. */ async function tryTo(callback) { if (store.dryRun) return const sessionName = 'tryTo' let result = false let isAutoRetriesEnabled = store.autoRetries return recorder.add( sessionName, () => { recorder.session.start(sessionName) isAutoRetriesEnabled = store.autoRetries if (isAutoRetriesEnabled) output.debug('Auto retries disabled inside tryTo effect') store.autoRetries = false callback() recorder.add(() => { result = true recorder.session.restore(sessionName) return result }) recorder.session.catch(err => { result = false const msg = err.inspect ? err.inspect() : err.toString() output.debug(`Unsuccessful try > ${msg}`) recorder.session.restore(sessionName) return result }) return recorder.add( 'result', () => { store.autoRetries = isAutoRetriesEnabled return result }, true, false, ) }, false, false, ) } export { hopeThat, retryTo, tryTo, within } export default { hopeThat, retryTo, tryTo, within, }