1
1
const recorder = require ( './recorder' )
2
2
const { debug } = require ( './output' )
3
3
const store = require ( './store' )
4
+ const event = require ( './event' )
4
5
5
6
/**
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.
61
9
*
62
10
* @async
63
11
* @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.
66
23
*
67
24
* @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
+ * });
71
29
*
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.
82
31
*/
83
32
async function hopeThat ( callback ) {
84
33
if ( store . dryRun ) return
@@ -100,6 +49,9 @@ async function hopeThat(callback) {
100
49
result = false
101
50
const msg = err . inspect ? err . inspect ( ) : err . toString ( )
102
51
debug ( `Unsuccessful assertion > ${ msg } ` )
52
+ event . dispatcher . once ( event . test . finished , test => {
53
+ test . notes . push ( { type : 'conditionalError' , text : msg } )
54
+ } )
103
55
recorder . session . restore ( sessionName )
104
56
return result
105
57
} )
@@ -118,6 +70,149 @@ async function hopeThat(callback) {
118
70
)
119
71
}
120
72
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
+
121
214
module . exports = {
122
215
hopeThat,
216
+ retryTo,
217
+ tryTo,
123
218
}
0 commit comments