Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move tryTo, retryTo to effects #4743

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions lib/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,210 @@ async function hopeThat(callback) {
)
}

/**
*
* @module retryTo
*
* `retryTo` which retries steps a few times before failing.
*
*
* Use it in your tests:
*
* const { retryTo } = require('codeceptjs/effects');
* ```js
* // retry these steps 5 times before failing
* await retryTo((tryNum) => {
* I.switchTo('#editor frame');
* I.click('Open');
* I.see('Opened')
* }, 5);
* ```
* Set polling interval as 3rd argument (200ms by default):
*
* ```js
* // retry these steps 5 times before failing
* await retryTo((tryNum) => {
* I.switchTo('#editor frame');
* I.click('Open');
* I.see('Opened')
* }, 5, 100);
* ```
*
* Disables retryFailedStep plugin for steps inside a block;
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved
*
* Use this plugin if:
*
* * you need repeat a set of actions in flaky tests
* * iframe was not rendered, so you need to retry switching to it
*
*
* #### Configuration
*
* * `pollInterval` - default interval between retries in ms. 200 by default.
*
*/
async function retryTo(callback, maxTries, pollInterval = 200) {
const sessionName = 'retryTo'

return new Promise((done, reject) => {
let tries = 1

function handleRetryException(err) {
recorder.throw(err)
reject(err)
}

const tryBlock = async () => {
tries++
recorder.session.start(`${sessionName} ${tries}`)
try {
await callback(tries)
} catch (err) {
handleRetryException(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) {
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)
})
})
}

/**
* @module tryTo
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved
*
* `tryTo` which all failed steps won't fail a test but will return true/false.
* It enables conditional assertions without terminating the test upon failure.
* This is particularly useful in scenarios like A/B testing, handling unexpected elements,
* or performing multiple assertions where you want to collect all results before deciding
* on the test outcome.
*
* ## Use Cases
*
* - **Multiple Conditional Assertions**: Perform several assertions and evaluate all their outcomes together.
* - **A/B Testing**: Handle different variants in A/B tests without failing the entire test upon one variant's failure.
* - **Unexpected Elements**: Manage elements that may or may not appear, such as "Accept Cookie" banners.
*
* ## Examples
*
* ### Multiple Conditional Assertions
*
* Add the assertion library:
* ```js
* const assert = require('assert');
* const { tryTo } = require('codeceptjs/effects');
* ```
*
* Use `hopeThat` with assertions:
* ```js
* const result1 = await tryTo(() => I.see('Hello, user'));
* const result2 = await tryTo(() => I.seeElement('.welcome'));
* assert.ok(result1 && result2, 'Assertions were not successful');
* ```
*
* ### Optional Click
*
* ```js
* const { tryTo } = require('codeceptjs/effects');
*
* I.amOnPage('/');
* await tryTo(() => I.click('Agree', '.cookies'));
* ```
*
* This function records the execution of a callback containing assertion logic.
* If the assertion fails, it logs the failure without stopping the test execution.
* It is useful for scenarios where multiple assertions are performed, and you want
* to evaluate all outcomes before deciding on the test result.
*
* ## Usage
*
* ```js
* const result = await tryTo(() => I.see('Welcome'));
*
* // If the text "Welcome" is on the page, result => true
* // If the text "Welcome" is not on the page, result => false
* ```
*
* @async
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved
* @function tryTo
* @param {Function} callback - The callback function.
* @returns {Promise<boolean | any>} - Resolves to `true` if the assertion is successful, or `false` if it fails.
*
* @example
* // Multiple Conditional Assertions
* const assert = require('assert');
* const { tryTo } = require('codeceptjs/effects');
*
* const result1 = await tryTo(() => I.see('Hello, user'));
* const result2 = await tryTo(() => I.seeElement('.welcome'));
* assert.ok(result1 && result2, 'Assertions were not successful');
*
* @example
* // Optional Click
* const { tryTo } = require('codeceptjs/effects');
*
* I.amOnPage('/');
* await tryTo(() => I.click('Agree', '.cookies'));
*/
async function tryTo(callback) {
if (store.dryRun) return
const sessionName = 'tryTo'

let result = false
return recorder.add(
sessionName,
() => {
recorder.session.start(sessionName)
store.tryTo = 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()
debug(`Unsuccessful try > ${msg}`)
recorder.session.restore(sessionName)
return result
})
return recorder.add(
'result',
() => {
store.tryTo = undefined
return result
},
true,
false,
)
},
false,
false,
)
}

module.exports = {
hopeThat,
retryTo,
tryTo,
}
144 changes: 18 additions & 126 deletions lib/plugin/retryTo.js
Original file line number Diff line number Diff line change
@@ -1,127 +1,19 @@
const recorder = require('../recorder')
const { debug } = require('../output')

const defaultConfig = {
registerGlobal: true,
pollInterval: 200,
}

/**
*
*
* Adds global `retryTo` which retries steps a few times before failing.
*
* Enable this plugin in `codecept.conf.js` (enabled by default for new setups):
*
* ```js
* plugins: {
* retryTo: {
* enabled: true
* }
* }
* ```
*
* Use it in your tests:
*
* ```js
* // retry these steps 5 times before failing
* await retryTo((tryNum) => {
* I.switchTo('#editor frame');
* I.click('Open');
* I.see('Opened')
* }, 5);
* ```
* Set polling interval as 3rd argument (200ms by default):
*
* ```js
* // retry these steps 5 times before failing
* await retryTo((tryNum) => {
* I.switchTo('#editor frame');
* I.click('Open');
* I.see('Opened')
* }, 5, 100);
* ```
*
* Default polling interval can be changed in a config:
*
* ```js
* plugins: {
* retryTo: {
* enabled: true,
* pollInterval: 500,
* }
* }
* ```
*
* Disables retryFailedStep plugin for steps inside a block;
*
* Use this plugin if:
*
* * you need repeat a set of actions in flaky tests
* * iframe was not rendered and you need to retry switching to it
*
*
* #### Configuration
*
* * `pollInterval` - default interval between retries in ms. 200 by default.
* * `registerGlobal` - to register `retryTo` function globally, true by default
*
* If `registerGlobal` is false you can use retryTo from the plugin:
*
* ```js
* const retryTo = codeceptjs.container.plugins('retryTo');
* ```
*
*/
module.exports = function (config) {
config = Object.assign(defaultConfig, config)
function retryTo(callback, maxTries, pollInterval = config.pollInterval) {
return new Promise((done, reject) => {
let tries = 1

function handleRetryException(err) {
recorder.throw(err)
reject(err)
}

const tryBlock = async () => {
tries++
recorder.session.start(`retryTo ${tries}`)
try {
await callback(tries)
} catch (err) {
handleRetryException(err)
}

// Call done if no errors
recorder.add(() => {
recorder.session.restore(`retryTo ${tries}`)
done(null)
})

// Catch errors and retry
recorder.session.catch(err => {
recorder.session.restore(`retryTo ${tries}`)
if (tries <= maxTries) {
debug(`Error ${err}... Retrying`)
recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval))
} else {
// if maxTries reached
handleRetryException(err)
}
})
}

recorder.add('retryTo', tryBlock).catch(err => {
console.error('An error occurred:', err)
done(null)
})
})
}

if (config.registerGlobal) {
global.retryTo = retryTo
}

return retryTo
module.exports = function () {
console.log(`
Deprecated Warning: 'retryTo' has been moved to the effects module.
You should update your tests to use it as follows:

\`\`\`javascript
const { retryTo } = require('codeceptjs/effects');

// Example: Retry these steps 5 times before failing
await retryTo((tryNum) => {
I.switchTo('#editor frame');
I.click('Open');
I.see('Opened');
}, 5);
\`\`\`

For more details, refer to the documentation.
`)
}
Loading
Loading