UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

175 lines (155 loc) 5.31 kB
import debugModule from 'debug' import event from '../event.js' import recorder from '../recorder.js' import store from '../store.js' const debug = debugModule('codeceptjs:retryFailedStep') const defaultConfig = { retries: 3, defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'], minTimeout: 150, maxTimeout: 10000, factor: 1.5, randomize: false, ignoredSteps: [], deferToScenarioRetries: true, } const RETRY_PRIORITIES = { MANUAL_STEP: 100, STEP_PLUGIN: 50, SCENARIO_CONFIG: 30, FEATURE_CONFIG: 20, HOOK_CONFIG: 10, } /** * Retries each failed step in a test. * * Add this plugin to config file: * * ```js * plugins: { * retryFailedStep: { * enabled: true * } * } * ``` * * * Run tests with plugin enabled: * * ``` * npx codeceptjs run --plugins retryFailedStep * ``` * * #### Configuration: * * * `retries` - number of retries (by default 3), * * `factor` - The exponential factor to use. Default is 1.5. * * `minTimeout` - The number of milliseconds before starting the first retry. Default is 150. * * `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000. * * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false. * * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes: * * `amOnPage` * * `wait*` * * `send*` * * `execute*` * * `run*` * * `have*` * * `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list. * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`. * To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well. * * `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries. * * #### Example * * ```js * plugins: { * retryFailedStep: { * enabled: true, * ignoredSteps: [ * 'scroll*', // ignore all scroll steps * /Cookie/, // ignore all steps with a Cookie in it (by regexp) * ] * } * } * ``` * * #### Disable Per Test * * This plugin can be disabled per test. In this case you will need to add `step.retry()` to all flaky steps: * * Use scenario configuration to disable plugin for a test * * ```js * Scenario('scenario tite', { disableRetryFailedStep: true }, () => { * // test goes here * }) * ``` * */ export default function (config) { config = Object.assign({}, defaultConfig, config) config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps) let enableRetry = false const when = err => { if (!enableRetry) return if (store.debugMode) return false if (!store.autoRetries) return false if (err && err.isTerminal) return false if (err && err.message && (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed'))) return false return true } config.when = when if (!recorder.retries.find(r => r === config)) { recorder.retries.push(config) } event.dispatcher.on(event.step.started, step => { if (!step.title) return for (const ignored of config.ignoredSteps) { if (step.title === ignored) return if (ignored instanceof RegExp) { if (step.title.match(ignored)) return } else if (ignored.indexOf('*') !== -1 && step.title.startsWith(ignored.slice(0, -1))) return } enableRetry = true }) event.dispatcher.on(event.step.passed, () => { enableRetry = false }) event.dispatcher.on(event.test.before, test => { if (!test.opts) test.opts = {} if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) { store.autoRetries = false return } const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1 const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN const scenarioPriority = test.opts.retryPriority || 0 if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) { store.autoRetries = false return } const hasManualRetries = recorder.retries.some(retry => retry !== config) if (hasManualRetries) { store.autoRetries = false return } store.autoRetries = true test.opts.conditionalRetries = config.retries test.opts.stepRetryPriority = stepRetryPriority debug('applying retries = %d for test %s', config.retries, test.title) recorder.retry(config) }) event.dispatcher.on(event.test.started, test => { if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return const hasManualRetries = recorder.retries.some(retry => retry !== config) if (hasManualRetries) return const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1 if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) { return } if (!store.autoRetries) { store.autoRetries = true test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries } }) }