11const recorder = require ( './recorder' )
22const { debug } = require ( './output' )
33const 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 */
8332async 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+
121214module . exports = {
122215 hopeThat,
216+ retryTo,
217+ tryTo,
123218}
0 commit comments