codeceptjs 3.7.0-beta.5 → 3.7.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,13 +5,13 @@
5
5
 
6
6
  ## Build Status
7
7
 
8
- | Type | Engine | Status |
9
- | --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
- | 🌐 Web | Playwright | [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) |
11
- | 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
12
- | 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
13
- | 🌐 Web | TestCafe | [![TestCafe Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml) |
14
- | 📱 Mobile | Appium | [![Appium V2 Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml) |
8
+ | Type | Engine | Status |
9
+ | --------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | 🌐 Web | Playwright | [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) |
11
+ | 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
12
+ | 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
13
+ | 🌐 Web | TestCafe | [![TestCafe Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml) |
14
+ | 📱 Mobile | Appium | [![Appium Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml) |
15
15
 
16
16
  # CodeceptJS [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua)
17
17
 
package/lib/effects.js CHANGED
@@ -1,84 +1,33 @@
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
  }
@@ -44,7 +44,7 @@ const vendorPrefix = {
44
44
  *
45
45
  * This helper should be configured in codecept.conf.ts or codecept.conf.js
46
46
  *
47
- * * `appiumV2`: set this to true if you want to run tests with AppiumV2. See more how to setup [here](https://codecept.io/mobile/#setting-up)
47
+ * * `appiumV2`: by default is true, set this to false if you want to run tests with AppiumV1. See more how to setup [here](https://codecept.io/mobile/#setting-up)
48
48
  * * `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage
49
49
  * * `host`: (default: 'localhost') Appium host
50
50
  * * `port`: (default: '4723') Appium port
@@ -124,7 +124,7 @@ const vendorPrefix = {
124
124
  * {
125
125
  * helpers: {
126
126
  * Appium: {
127
- * appiumV2: true,
127
+ * appiumV2: true, // By default is true, set to false if you want to run against Appium v1
128
128
  * host: "hub-cloud.browserstack.com",
129
129
  * port: 4444,
130
130
  * user: process.env.BROWSERSTACK_USER,
@@ -178,14 +178,12 @@ class Appium extends Webdriver {
178
178
  super(config)
179
179
 
180
180
  this.isRunning = false
181
- if (config.appiumV2 === true) {
182
- this.appiumV2 = true
183
- }
181
+ this.appiumV2 = config.appiumV2 || true
184
182
  this.axios = axios.create()
185
183
 
186
184
  webdriverio = require('webdriverio')
187
185
  if (!config.appiumV2) {
188
- console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.')
186
+ console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Appium 2.x is used by default.')
189
187
  console.log('More info: https://bit.ly/appium-v2-migration')
190
188
  console.log('This Appium 1.x support will be removed in next major release.')
191
189
  }
@@ -24,6 +24,7 @@ const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTr
24
24
 
25
25
  const SHADOW = 'shadow'
26
26
  const webRoot = 'body'
27
+ let browserLogs = []
27
28
 
28
29
  /**
29
30
  * ## Configuration
@@ -621,6 +622,10 @@ class WebDriver extends Helper {
621
622
  }
622
623
 
623
624
  this.browser.on('dialog', () => {})
625
+
626
+ await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
627
+ this.browser.on('log.entryAdded', logEvents)
628
+
624
629
  return this.browser
625
630
  }
626
631
 
@@ -658,6 +663,7 @@ class WebDriver extends Helper {
658
663
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
659
664
  })
660
665
  await this.closeOtherTabs()
666
+ browserLogs = []
661
667
  return this.browser
662
668
  }
663
669
 
@@ -1522,11 +1528,7 @@ class WebDriver extends Helper {
1522
1528
  * {{> grabBrowserLogs }}
1523
1529
  */
1524
1530
  async grabBrowserLogs() {
1525
- if (this.browser.isW3C) {
1526
- this.debug('Logs not available in W3C specification')
1527
- return
1528
- }
1529
- return this.browser.getLogs('browser')
1531
+ return browserLogs
1530
1532
  }
1531
1533
 
1532
1534
  /**
@@ -1788,18 +1790,25 @@ class WebDriver extends Helper {
1788
1790
 
1789
1791
  if (browser) {
1790
1792
  this.debug(`Screenshot of ${sessionName} session has been saved to ${outputFile}`)
1791
- return browser.saveScreenshot(outputFile)
1793
+ await browser.saveScreenshot(outputFile)
1792
1794
  }
1793
1795
  }
1794
1796
  }
1795
1797
 
1796
1798
  if (!fullPage) {
1797
1799
  this.debug(`Screenshot has been saved to ${outputFile}`)
1798
- return this.browser.saveScreenshot(outputFile)
1800
+ await this.browser.saveScreenshot(outputFile)
1799
1801
  }
1800
1802
 
1801
1803
  const originalWindowSize = await this.browser.getWindowSize()
1802
1804
 
1805
+ // this case running on device, so we could not set the windowSize
1806
+ if (this.browser.isMobile) {
1807
+ this.debug(`Screenshot has been saved to ${outputFile}, size: ${originalWindowSize.width}x${originalWindowSize.height}`)
1808
+ const buffer = await this.browser.saveScreenshot(outputFile)
1809
+ return buffer
1810
+ }
1811
+
1803
1812
  let { width, height } = await this.browser
1804
1813
  .execute(function () {
1805
1814
  return {
@@ -3134,4 +3143,8 @@ function prepareLocateFn(context) {
3134
3143
  }
3135
3144
  }
3136
3145
 
3146
+ function logEvents(event) {
3147
+ browserLogs.push(event.text) // add log message to the array
3148
+ }
3149
+
3137
3150
  module.exports = WebDriver
package/lib/pause.js CHANGED
@@ -84,12 +84,10 @@ function pauseSession(passedObject = {}) {
84
84
  })
85
85
  return new Promise(resolve => {
86
86
  finish = resolve
87
- // eslint-disable-next-line
88
87
  return askForStep()
89
88
  })
90
89
  }
91
90
 
92
- /* eslint-disable */
93
91
  async function parseInput(cmd) {
94
92
  rl.pause()
95
93
  next = false
@@ -198,7 +196,6 @@ async function parseInput(cmd) {
198
196
  recorder.add('ask for next step', askForStep)
199
197
  nextStep()
200
198
  }
201
- /* eslint-enable */
202
199
 
203
200
  function askForStep() {
204
201
  return new Promise(resolve => {
@@ -1,127 +1,19 @@
1
- const recorder = require('../recorder')
2
- const { debug } = require('../output')
3
-
4
- const defaultConfig = {
5
- registerGlobal: true,
6
- pollInterval: 200,
7
- }
8
-
9
- /**
10
- *
11
- *
12
- * Adds global `retryTo` which retries steps a few times before failing.
13
- *
14
- * Enable this plugin in `codecept.conf.js` (enabled by default for new setups):
15
- *
16
- * ```js
17
- * plugins: {
18
- * retryTo: {
19
- * enabled: true
20
- * }
21
- * }
22
- * ```
23
- *
24
- * Use it in your tests:
25
- *
26
- * ```js
27
- * // retry these steps 5 times before failing
28
- * await retryTo((tryNum) => {
29
- * I.switchTo('#editor frame');
30
- * I.click('Open');
31
- * I.see('Opened')
32
- * }, 5);
33
- * ```
34
- * Set polling interval as 3rd argument (200ms by default):
35
- *
36
- * ```js
37
- * // retry these steps 5 times before failing
38
- * await retryTo((tryNum) => {
39
- * I.switchTo('#editor frame');
40
- * I.click('Open');
41
- * I.see('Opened')
42
- * }, 5, 100);
43
- * ```
44
- *
45
- * Default polling interval can be changed in a config:
46
- *
47
- * ```js
48
- * plugins: {
49
- * retryTo: {
50
- * enabled: true,
51
- * pollInterval: 500,
52
- * }
53
- * }
54
- * ```
55
- *
56
- * Disables retryFailedStep plugin for steps inside a block;
57
- *
58
- * Use this plugin if:
59
- *
60
- * * you need repeat a set of actions in flaky tests
61
- * * iframe was not rendered and you need to retry switching to it
62
- *
63
- *
64
- * #### Configuration
65
- *
66
- * * `pollInterval` - default interval between retries in ms. 200 by default.
67
- * * `registerGlobal` - to register `retryTo` function globally, true by default
68
- *
69
- * If `registerGlobal` is false you can use retryTo from the plugin:
70
- *
71
- * ```js
72
- * const retryTo = codeceptjs.container.plugins('retryTo');
73
- * ```
74
- *
75
- */
76
- module.exports = function (config) {
77
- config = Object.assign(defaultConfig, config)
78
- function retryTo(callback, maxTries, pollInterval = config.pollInterval) {
79
- return new Promise((done, reject) => {
80
- let tries = 1
81
-
82
- function handleRetryException(err) {
83
- recorder.throw(err)
84
- reject(err)
85
- }
86
-
87
- const tryBlock = async () => {
88
- tries++
89
- recorder.session.start(`retryTo ${tries}`)
90
- try {
91
- await callback(tries)
92
- } catch (err) {
93
- handleRetryException(err)
94
- }
95
-
96
- // Call done if no errors
97
- recorder.add(() => {
98
- recorder.session.restore(`retryTo ${tries}`)
99
- done(null)
100
- })
101
-
102
- // Catch errors and retry
103
- recorder.session.catch(err => {
104
- recorder.session.restore(`retryTo ${tries}`)
105
- if (tries <= maxTries) {
106
- debug(`Error ${err}... Retrying`)
107
- recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval))
108
- } else {
109
- // if maxTries reached
110
- handleRetryException(err)
111
- }
112
- })
113
- }
114
-
115
- recorder.add('retryTo', tryBlock).catch(err => {
116
- console.error('An error occurred:', err)
117
- done(null)
118
- })
119
- })
120
- }
121
-
122
- if (config.registerGlobal) {
123
- global.retryTo = retryTo
124
- }
125
-
126
- return retryTo
1
+ module.exports = function () {
2
+ console.log(`
3
+ Deprecated Warning: 'retryTo' has been moved to the effects module.
4
+ You should update your tests to use it as follows:
5
+
6
+ \`\`\`javascript
7
+ const { retryTo } = require('codeceptjs/effects');
8
+
9
+ // Example: Retry these steps 5 times before failing
10
+ await retryTo((tryNum) => {
11
+ I.switchTo('#editor frame');
12
+ I.click('Open');
13
+ I.see('Opened');
14
+ }, 5);
15
+ \`\`\`
16
+
17
+ For more details, refer to the documentation.
18
+ `)
127
19
  }