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 5 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
235 changes: 165 additions & 70 deletions lib/effects.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,33 @@
const recorder = require('./recorder')
const { debug } = require('./output')
const store = require('./store')
const event = require('./event')

/**
* @module hopeThat
*
* `hopeThat` is a utility function for CodeceptJS tests that allows for soft assertions.
* 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 { hopeThat } = require('codeceptjs/effects');
* ```
*
* Use `hopeThat` with assertions:
* ```js
* const result1 = await hopeThat(() => I.see('Hello, user'));
* const result2 = await hopeThat(() => I.seeElement('.welcome'));
* assert.ok(result1 && result2, 'Assertions were not successful');
* ```
*
* ### Optional Click
*
* ```js
* const { hopeThat } = require('codeceptjs/effects');
*
* I.amOnPage('/');
* await hopeThat(() => I.click('Agree', '.cookies'));
* ```
*
* Performs a soft assertion within CodeceptJS tests.
*
* 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 hopeThat(() => I.see('Welcome'));
*
* // If the text "Welcome" is on the page, result => true
* // If the text "Welcome" is not on the page, result => false
* ```
* 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 soft assertion logic.
* @returns {Promise<boolean | any>} - Resolves to `true` if the assertion is successful, or `false` if it fails.
* @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
* // Multiple Conditional Assertions
* const assert = require('assert');
* const { hopeThat } = require('codeceptjs/effects');
* const { hopeThat } = require('codeceptjs/effects')
* await hopeThat(() => {
* I.see('Welcome'); // Perform a soft assertion
* });
*
* const result1 = await hopeThat(() => I.see('Hello, user'));
* const result2 = await hopeThat(() => I.seeElement('.welcome'));
* assert.ok(result1 && result2, 'Assertions were not successful');
*
* @example
* // Optional Click
* const { hopeThat } = require('codeceptjs/effects');
*
* I.amOnPage('/');
* await hopeThat(() => I.click('Agree', '.cookies'));
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
*/
async function hopeThat(callback) {
if (store.dryRun) return
Expand All @@ -100,6 +49,9 @@ async function hopeThat(callback) {
result = false
const msg = err.inspect ? err.inspect() : err.toString()
debug(`Unsuccessful assertion > ${msg}`)
event.dispatcher.on(event.test.finished, test => {
DavertMik marked this conversation as resolved.
Show resolved Hide resolved
test.notes.push({ type: 'conditionalError', text: msg })
})
recorder.session.restore(sessionName)
return result
})
Expand All @@ -118,6 +70,149 @@ async function hopeThat(callback) {
)
}

/**
* 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 { hopeThat } = 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 = 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)
})
})
}

/**
* 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
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved
* @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
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,
}
Loading
Loading