Skip to content

Commit

Permalink
move tryTo, retryTo to effects (#4743)
Browse files Browse the repository at this point in the history
  • Loading branch information
kobenguyent authored Jan 15, 2025
1 parent 4c68dd7 commit 117c6c1
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 482 deletions.
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.once(event.test.finished, test => {
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
* @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

0 comments on commit 117c6c1

Please sign in to comment.