Skip to content

Commit 117c6c1

Browse files
authored
move tryTo, retryTo to effects (#4743)
1 parent 4c68dd7 commit 117c6c1

File tree

11 files changed

+300
-482
lines changed

11 files changed

+300
-482
lines changed

lib/effects.js

Lines changed: 165 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,33 @@
11
const recorder = require('./recorder')
22
const { debug } = require('./output')
33
const store = require('./store')
4+
const event = require('./event')
45

56
/**
6-
* @module hopeThat
7-
*
8-
* `hopeThat` is a utility function for CodeceptJS tests that allows for soft assertions.
9-
* It enables conditional assertions without terminating the test upon failure.
10-
* This is particularly useful in scenarios like A/B testing, handling unexpected elements,
11-
* or performing multiple assertions where you want to collect all results before deciding
12-
* on the test outcome.
13-
*
14-
* ## Use Cases
15-
*
16-
* - **Multiple Conditional Assertions**: Perform several assertions and evaluate all their outcomes together.
17-
* - **A/B Testing**: Handle different variants in A/B tests without failing the entire test upon one variant's failure.
18-
* - **Unexpected Elements**: Manage elements that may or may not appear, such as "Accept Cookie" banners.
19-
*
20-
* ## Examples
21-
*
22-
* ### Multiple Conditional Assertions
23-
*
24-
* Add the assertion library:
25-
* ```js
26-
* const assert = require('assert');
27-
* const { hopeThat } = require('codeceptjs/effects');
28-
* ```
29-
*
30-
* Use `hopeThat` with assertions:
31-
* ```js
32-
* const result1 = await hopeThat(() => I.see('Hello, user'));
33-
* const result2 = await hopeThat(() => I.seeElement('.welcome'));
34-
* assert.ok(result1 && result2, 'Assertions were not successful');
35-
* ```
36-
*
37-
* ### Optional Click
38-
*
39-
* ```js
40-
* const { hopeThat } = require('codeceptjs/effects');
41-
*
42-
* I.amOnPage('/');
43-
* await hopeThat(() => I.click('Agree', '.cookies'));
44-
* ```
45-
*
46-
* Performs a soft assertion within CodeceptJS tests.
47-
*
48-
* This function records the execution of a callback containing assertion logic.
49-
* If the assertion fails, it logs the failure without stopping the test execution.
50-
* It is useful for scenarios where multiple assertions are performed, and you want
51-
* to evaluate all outcomes before deciding on the test result.
52-
*
53-
* ## Usage
54-
*
55-
* ```js
56-
* const result = await hopeThat(() => I.see('Welcome'));
57-
*
58-
* // If the text "Welcome" is on the page, result => true
59-
* // If the text "Welcome" is not on the page, result => false
60-
* ```
7+
* A utility function for CodeceptJS tests that acts as a soft assertion.
8+
* Executes a callback within a recorded session, ensuring errors are handled gracefully without failing the test immediately.
619
*
6210
* @async
6311
* @function hopeThat
64-
* @param {Function} callback - The callback function containing the soft assertion logic.
65-
* @returns {Promise<boolean | any>} - Resolves to `true` if the assertion is successful, or `false` if it fails.
12+
* @param {Function} callback - The callback function containing the logic to validate.
13+
* This function should perform the desired assertion or condition check.
14+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the assertion or condition was successful,
15+
* or `false` if an error occurred.
16+
*
17+
* @description
18+
* - Designed for use in CodeceptJS tests as a "soft assertion."
19+
* Unlike standard assertions, it does not stop the test execution on failure.
20+
* - Starts a new recorder session named 'hopeThat' and manages state restoration.
21+
* - Logs errors and attaches them as notes to the test, enabling post-test reporting of soft assertion failures.
22+
* - Resets the `store.hopeThat` flag after the execution, ensuring clean state for subsequent operations.
6623
*
6724
* @example
68-
* // Multiple Conditional Assertions
69-
* const assert = require('assert');
70-
* const { hopeThat } = require('codeceptjs/effects');
25+
* const { hopeThat } = require('codeceptjs/effects')
26+
* await hopeThat(() => {
27+
* I.see('Welcome'); // Perform a soft assertion
28+
* });
7129
*
72-
* const result1 = await hopeThat(() => I.see('Hello, user'));
73-
* const result2 = await hopeThat(() => I.seeElement('.welcome'));
74-
* assert.ok(result1 && result2, 'Assertions were not successful');
75-
*
76-
* @example
77-
* // Optional Click
78-
* const { hopeThat } = require('codeceptjs/effects');
79-
*
80-
* I.amOnPage('/');
81-
* await hopeThat(() => I.click('Agree', '.cookies'));
30+
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
8231
*/
8332
async function hopeThat(callback) {
8433
if (store.dryRun) return
@@ -100,6 +49,9 @@ async function hopeThat(callback) {
10049
result = false
10150
const msg = err.inspect ? err.inspect() : err.toString()
10251
debug(`Unsuccessful assertion > ${msg}`)
52+
event.dispatcher.once(event.test.finished, test => {
53+
test.notes.push({ type: 'conditionalError', text: msg })
54+
})
10355
recorder.session.restore(sessionName)
10456
return result
10557
})
@@ -118,6 +70,149 @@ async function hopeThat(callback) {
11870
)
11971
}
12072

73+
/**
74+
* A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
75+
*
76+
* @async
77+
* @function retryTo
78+
* @param {Function} callback - The function to execute, which will be retried upon failure.
79+
* Receives the current retry count as an argument.
80+
* @param {number} maxTries - The maximum number of attempts to retry the callback.
81+
* @param {number} [pollInterval=200] - The delay (in milliseconds) between retry attempts.
82+
* @returns {Promise<void|any>} A promise that resolves when the callback executes successfully, or rejects after reaching the maximum retries.
83+
*
84+
* @description
85+
* - This function is designed for use in CodeceptJS tests to handle intermittent or flaky test steps.
86+
* - Starts a new recorder session for each retry attempt, ensuring proper state management and error handling.
87+
* - Logs errors and retries the callback until it either succeeds or the maximum number of attempts is reached.
88+
* - Restores the session state after each attempt, whether successful or not.
89+
*
90+
* @example
91+
* const { hopeThat } = require('codeceptjs/effects')
92+
* await retryTo((tries) => {
93+
* if (tries < 3) {
94+
* I.see('Non-existent element'); // Simulates a failure
95+
* } else {
96+
* I.see('Welcome'); // Succeeds on the 3rd attempt
97+
* }
98+
* }, 5, 300); // Retry up to 5 times, with a 300ms interval
99+
*
100+
* @throws Will reject with the last error encountered if the maximum retries are exceeded.
101+
*/
102+
async function retryTo(callback, maxTries, pollInterval = 200) {
103+
const sessionName = 'retryTo'
104+
105+
return new Promise((done, reject) => {
106+
let tries = 1
107+
108+
function handleRetryException(err) {
109+
recorder.throw(err)
110+
reject(err)
111+
}
112+
113+
const tryBlock = async () => {
114+
tries++
115+
recorder.session.start(`${sessionName} ${tries}`)
116+
try {
117+
await callback(tries)
118+
} catch (err) {
119+
handleRetryException(err)
120+
}
121+
122+
// Call done if no errors
123+
recorder.add(() => {
124+
recorder.session.restore(`${sessionName} ${tries}`)
125+
done(null)
126+
})
127+
128+
// Catch errors and retry
129+
recorder.session.catch(err => {
130+
recorder.session.restore(`${sessionName} ${tries}`)
131+
if (tries <= maxTries) {
132+
debug(`Error ${err}... Retrying`)
133+
recorder.add(`${sessionName} ${tries}`, () => setTimeout(tryBlock, pollInterval))
134+
} else {
135+
// if maxTries reached
136+
handleRetryException(err)
137+
}
138+
})
139+
}
140+
141+
recorder.add(sessionName, tryBlock).catch(err => {
142+
console.error('An error occurred:', err)
143+
done(null)
144+
})
145+
})
146+
}
147+
148+
/**
149+
* A CodeceptJS utility function to attempt a step or callback without failing the test.
150+
* If the step fails, the test continues execution without interruption, and the result is logged.
151+
*
152+
* @async
153+
* @function tryTo
154+
* @param {Function} callback - The function to execute, which may succeed or fail.
155+
* This function contains the logic to be attempted.
156+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the step succeeds, or `false` if it fails.
157+
*
158+
* @description
159+
* - Useful for scenarios where certain steps are optional or their failure should not interrupt the test flow.
160+
* - Starts a new recorder session named 'tryTo' for isolation and error handling.
161+
* - Captures errors during execution and logs them for debugging purposes.
162+
* - Ensures the `store.tryTo` flag is reset after execution to maintain a clean state.
163+
*
164+
* @example
165+
* const { tryTo } = require('codeceptjs/effects')
166+
* const wasSuccessful = await tryTo(() => {
167+
* I.see('Welcome'); // Attempt to find an element on the page
168+
* });
169+
*
170+
* if (!wasSuccessful) {
171+
* I.say('Optional step failed, but test continues.');
172+
* }
173+
*
174+
* @throws Will handle errors internally, logging them and returning `false` as the result.
175+
*/
176+
async function tryTo(callback) {
177+
if (store.dryRun) return
178+
const sessionName = 'tryTo'
179+
180+
let result = false
181+
return recorder.add(
182+
sessionName,
183+
() => {
184+
recorder.session.start(sessionName)
185+
store.tryTo = true
186+
callback()
187+
recorder.add(() => {
188+
result = true
189+
recorder.session.restore(sessionName)
190+
return result
191+
})
192+
recorder.session.catch(err => {
193+
result = false
194+
const msg = err.inspect ? err.inspect() : err.toString()
195+
debug(`Unsuccessful try > ${msg}`)
196+
recorder.session.restore(sessionName)
197+
return result
198+
})
199+
return recorder.add(
200+
'result',
201+
() => {
202+
store.tryTo = undefined
203+
return result
204+
},
205+
true,
206+
false,
207+
)
208+
},
209+
false,
210+
false,
211+
)
212+
}
213+
121214
module.exports = {
122215
hopeThat,
216+
retryTo,
217+
tryTo,
123218
}

0 commit comments

Comments
 (0)