codeceptjs 3.6.10 → 3.7.0-beta.10

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.
Files changed (127) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +9 -2
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +66 -102
  5. package/lib/ai.js +130 -121
  6. package/lib/assert/empty.js +3 -5
  7. package/lib/assert/equal.js +4 -7
  8. package/lib/assert/include.js +4 -6
  9. package/lib/assert/throws.js +2 -4
  10. package/lib/assert/truth.js +2 -2
  11. package/lib/codecept.js +87 -83
  12. package/lib/command/check.js +186 -0
  13. package/lib/command/configMigrate.js +2 -4
  14. package/lib/command/definitions.js +8 -26
  15. package/lib/command/generate.js +10 -14
  16. package/lib/command/gherkin/snippets.js +10 -8
  17. package/lib/command/gherkin/steps.js +1 -1
  18. package/lib/command/info.js +1 -3
  19. package/lib/command/init.js +8 -12
  20. package/lib/command/interactive.js +2 -2
  21. package/lib/command/list.js +1 -1
  22. package/lib/command/run-multiple.js +12 -35
  23. package/lib/command/run-workers.js +5 -57
  24. package/lib/command/utils.js +5 -6
  25. package/lib/command/workers/runTests.js +68 -232
  26. package/lib/container.js +354 -237
  27. package/lib/data/context.js +10 -13
  28. package/lib/data/dataScenarioConfig.js +8 -8
  29. package/lib/data/dataTableArgument.js +6 -6
  30. package/lib/data/table.js +5 -11
  31. package/lib/effects.js +218 -0
  32. package/lib/els.js +158 -0
  33. package/lib/event.js +19 -17
  34. package/lib/heal.js +88 -80
  35. package/lib/helper/AI.js +2 -1
  36. package/lib/helper/ApiDataFactory.js +3 -6
  37. package/lib/helper/Appium.js +45 -51
  38. package/lib/helper/FileSystem.js +3 -3
  39. package/lib/helper/GraphQLDataFactory.js +3 -3
  40. package/lib/helper/JSONResponse.js +57 -37
  41. package/lib/helper/Nightmare.js +35 -53
  42. package/lib/helper/Playwright.js +211 -252
  43. package/lib/helper/Protractor.js +54 -77
  44. package/lib/helper/Puppeteer.js +139 -232
  45. package/lib/helper/REST.js +5 -17
  46. package/lib/helper/TestCafe.js +21 -44
  47. package/lib/helper/WebDriver.js +131 -169
  48. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  49. package/lib/listener/emptyRun.js +55 -0
  50. package/lib/listener/exit.js +7 -10
  51. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  52. package/lib/listener/globalTimeout.js +165 -0
  53. package/lib/listener/helpers.js +15 -15
  54. package/lib/listener/mocha.js +1 -1
  55. package/lib/listener/result.js +12 -0
  56. package/lib/listener/steps.js +20 -18
  57. package/lib/listener/store.js +20 -0
  58. package/lib/mocha/asyncWrapper.js +216 -0
  59. package/lib/{interfaces → mocha}/bdd.js +3 -3
  60. package/lib/mocha/cli.js +308 -0
  61. package/lib/mocha/factory.js +104 -0
  62. package/lib/{interfaces → mocha}/featureConfig.js +24 -12
  63. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  64. package/lib/mocha/hooks.js +112 -0
  65. package/lib/mocha/index.js +12 -0
  66. package/lib/mocha/inject.js +29 -0
  67. package/lib/{interfaces → mocha}/scenarioConfig.js +21 -6
  68. package/lib/mocha/suite.js +81 -0
  69. package/lib/mocha/test.js +159 -0
  70. package/lib/mocha/types.d.ts +42 -0
  71. package/lib/mocha/ui.js +219 -0
  72. package/lib/output.js +82 -62
  73. package/lib/pause.js +155 -138
  74. package/lib/plugin/analyze.js +349 -0
  75. package/lib/plugin/autoDelay.js +6 -6
  76. package/lib/plugin/autoLogin.js +6 -7
  77. package/lib/plugin/commentStep.js +6 -1
  78. package/lib/plugin/coverage.js +10 -19
  79. package/lib/plugin/customLocator.js +3 -3
  80. package/lib/plugin/customReporter.js +52 -0
  81. package/lib/plugin/eachElement.js +1 -1
  82. package/lib/plugin/fakerTransform.js +1 -1
  83. package/lib/plugin/heal.js +36 -9
  84. package/lib/plugin/pageInfo.js +140 -0
  85. package/lib/plugin/retryFailedStep.js +4 -4
  86. package/lib/plugin/retryTo.js +18 -118
  87. package/lib/plugin/screenshotOnFail.js +17 -49
  88. package/lib/plugin/selenoid.js +15 -35
  89. package/lib/plugin/standardActingHelpers.js +4 -1
  90. package/lib/plugin/stepByStepReport.js +56 -17
  91. package/lib/plugin/stepTimeout.js +5 -12
  92. package/lib/plugin/subtitles.js +4 -4
  93. package/lib/plugin/tryTo.js +17 -107
  94. package/lib/plugin/wdio.js +8 -10
  95. package/lib/recorder.js +146 -125
  96. package/lib/rerun.js +43 -42
  97. package/lib/result.js +161 -0
  98. package/lib/secret.js +1 -1
  99. package/lib/step/base.js +228 -0
  100. package/lib/step/config.js +50 -0
  101. package/lib/step/func.js +46 -0
  102. package/lib/step/helper.js +50 -0
  103. package/lib/step/meta.js +99 -0
  104. package/lib/step/record.js +74 -0
  105. package/lib/step/retry.js +11 -0
  106. package/lib/step/section.js +55 -0
  107. package/lib/step.js +21 -332
  108. package/lib/steps.js +50 -0
  109. package/lib/store.js +10 -2
  110. package/lib/template/heal.js +2 -11
  111. package/lib/timeout.js +66 -0
  112. package/lib/utils.js +317 -216
  113. package/lib/within.js +73 -55
  114. package/lib/workers.js +259 -275
  115. package/package.json +56 -54
  116. package/typings/index.d.ts +175 -186
  117. package/typings/promiseBasedTypes.d.ts +164 -17
  118. package/typings/types.d.ts +284 -115
  119. package/lib/cli.js +0 -256
  120. package/lib/helper/ExpectHelper.js +0 -391
  121. package/lib/helper/SoftExpectHelper.js +0 -381
  122. package/lib/listener/artifacts.js +0 -19
  123. package/lib/listener/timeout.js +0 -109
  124. package/lib/mochaFactory.js +0 -113
  125. package/lib/plugin/debugErrors.js +0 -67
  126. package/lib/scenario.js +0 -224
  127. package/lib/ui.js +0 -236
@@ -13,8 +13,8 @@ module.exports = function (context) {
13
13
  fn = opts
14
14
  opts = {}
15
15
  }
16
- opts.data = data.map((dataRow) => dataRow.data)
17
- data.forEach((dataRow) => {
16
+ opts.data = data.map(dataRow => dataRow.data)
17
+ data.forEach(dataRow => {
18
18
  const dataTitle = replaceTitle(title, dataRow)
19
19
  if (dataRow.skip) {
20
20
  context.xScenario(dataTitle)
@@ -32,8 +32,8 @@ module.exports = function (context) {
32
32
  fn = opts
33
33
  opts = {}
34
34
  }
35
- opts.data = data.map((dataRow) => dataRow.data)
36
- data.forEach((dataRow) => {
35
+ opts.data = data.map(dataRow => dataRow.data)
36
+ data.forEach(dataRow => {
37
37
  const dataTitle = replaceTitle(title, dataRow)
38
38
  if (dataRow.skip) {
39
39
  context.xScenario(dataTitle)
@@ -51,8 +51,8 @@ module.exports = function (context) {
51
51
  context.xData = function (dataTable) {
52
52
  const data = detectDataType(dataTable)
53
53
  return {
54
- Scenario: (title) => {
55
- data.forEach((dataRow) => {
54
+ Scenario: title => {
55
+ data.forEach(dataRow => {
56
56
  const dataTitle = replaceTitle(title, dataRow)
57
57
  context.xScenario(dataTitle)
58
58
  })
@@ -69,10 +69,7 @@ function replaceTitle(title, dataRow) {
69
69
 
70
70
  // if `dataRow` is object and has own `toString()` method,
71
71
  // it should be printed
72
- if (
73
- Object.prototype.toString.call(dataRow.data) === Object().toString() &&
74
- dataRow.data.toString() !== Object().toString()
75
- ) {
72
+ if (Object.prototype.toString.call(dataRow.data) === Object().toString() && dataRow.data.toString() !== Object().toString()) {
76
73
  return `${title} | ${dataRow.data}`
77
74
  }
78
75
 
@@ -102,7 +99,7 @@ function detectDataType(dataTable) {
102
99
  return dataTable()
103
100
  }
104
101
  if (Array.isArray(dataTable)) {
105
- return dataTable.map((item) => {
102
+ return dataTable.map(item => {
106
103
  if (isTableDataRow(item)) {
107
104
  return item
108
105
  }
@@ -117,10 +114,10 @@ function detectDataType(dataTable) {
117
114
  }
118
115
 
119
116
  function maskSecretInTitle(scenarios) {
120
- scenarios.forEach((scenario) => {
117
+ scenarios.forEach(scenario => {
121
118
  const res = []
122
119
 
123
- scenario.test.title.split(',').forEach((item) => {
120
+ scenario.test.title.split(',').forEach(item => {
124
121
  res.push(item.replace(/{"_secret":"(.*)"}/, '"*****"'))
125
122
  })
126
123
 
@@ -10,7 +10,7 @@ class DataScenarioConfig {
10
10
  * @param {*} err
11
11
  */
12
12
  throws(err) {
13
- this.scenarios.forEach((scenario) => scenario.throws(err))
13
+ this.scenarios.forEach(scenario => scenario.throws(err))
14
14
  return this
15
15
  }
16
16
 
@@ -21,7 +21,7 @@ class DataScenarioConfig {
21
21
  *
22
22
  */
23
23
  fails() {
24
- this.scenarios.forEach((scenario) => scenario.fails())
24
+ this.scenarios.forEach(scenario => scenario.fails())
25
25
  return this
26
26
  }
27
27
 
@@ -31,7 +31,7 @@ class DataScenarioConfig {
31
31
  * @param {*} retries
32
32
  */
33
33
  retry(retries) {
34
- this.scenarios.forEach((scenario) => scenario.retry(retries))
34
+ this.scenarios.forEach(scenario => scenario.retry(retries))
35
35
  return this
36
36
  }
37
37
 
@@ -40,7 +40,7 @@ class DataScenarioConfig {
40
40
  * @param {*} timeout
41
41
  */
42
42
  timeout(timeout) {
43
- this.scenarios.forEach((scenario) => scenario.timeout(timeout))
43
+ this.scenarios.forEach(scenario => scenario.timeout(timeout))
44
44
  return this
45
45
  }
46
46
 
@@ -49,7 +49,7 @@ class DataScenarioConfig {
49
49
  * Helper name can be omitted and values will be applied to first helper.
50
50
  */
51
51
  config(helper, obj) {
52
- this.scenarios.forEach((scenario) => scenario.config(helper, obj))
52
+ this.scenarios.forEach(scenario => scenario.config(helper, obj))
53
53
  return this
54
54
  }
55
55
 
@@ -58,7 +58,7 @@ class DataScenarioConfig {
58
58
  * @param {*} tagName
59
59
  */
60
60
  tag(tagName) {
61
- this.scenarios.forEach((scenario) => scenario.tag(tagName))
61
+ this.scenarios.forEach(scenario => scenario.tag(tagName))
62
62
  return this
63
63
  }
64
64
 
@@ -67,7 +67,7 @@ class DataScenarioConfig {
67
67
  * @param {*} obj
68
68
  */
69
69
  inject(obj) {
70
- this.scenarios.forEach((scenario) => scenario.inject(obj))
70
+ this.scenarios.forEach(scenario => scenario.inject(obj))
71
71
  return this
72
72
  }
73
73
 
@@ -76,7 +76,7 @@ class DataScenarioConfig {
76
76
  * @param {*} dependencies
77
77
  */
78
78
  injectDependencies(dependencies) {
79
- this.scenarios.forEach((scenario) => scenario.injectDependencies(dependencies))
79
+ this.scenarios.forEach(scenario => scenario.injectDependencies(dependencies))
80
80
  return this
81
81
  }
82
82
  }
@@ -5,8 +5,8 @@
5
5
  class DataTableArgument {
6
6
  /** @param {*} gherkinDataTable */
7
7
  constructor(gherkinDataTable) {
8
- this.rawData = gherkinDataTable.rows.map((row) => {
9
- return row.cells.map((cell) => {
8
+ this.rawData = gherkinDataTable.rows.map(row => {
9
+ return row.cells.map(cell => {
10
10
  return cell.value
11
11
  })
12
12
  })
@@ -34,7 +34,7 @@ class DataTableArgument {
34
34
  hashes() {
35
35
  const copy = this.raw()
36
36
  const header = copy.shift()
37
- return copy.map((row) => {
37
+ return copy.map(row => {
38
38
  const r = {}
39
39
  row.forEach((cell, index) => (r[header[index]] = cell))
40
40
  return r
@@ -47,19 +47,19 @@ class DataTableArgument {
47
47
  */
48
48
  rowsHash() {
49
49
  const rows = this.raw()
50
- const everyRowHasTwoColumns = rows.every((row) => row.length === 2)
50
+ const everyRowHasTwoColumns = rows.every(row => row.length === 2)
51
51
  if (!everyRowHasTwoColumns) {
52
52
  throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns')
53
53
  }
54
54
  /** @type {Record<string, string>} */
55
55
  const result = {}
56
- rows.forEach((x) => (result[x[0]] = x[1]))
56
+ rows.forEach(x => (result[x[0]] = x[1]))
57
57
  return result
58
58
  }
59
59
 
60
60
  /** Transposed the data */
61
61
  transpose() {
62
- this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i]))
62
+ this.rawData = this.rawData[0].map((x, i) => this.rawData.map(y => y[i]))
63
63
  }
64
64
  }
65
65
 
package/lib/data/table.js CHANGED
@@ -10,13 +10,10 @@ class DataTable {
10
10
 
11
11
  /** @param {Array<*>} array */
12
12
  add(array) {
13
- if (array.length !== this.array.length)
14
- throw new Error(
15
- `There is too many elements in given data array. Please provide data in this format: ${this.array}`,
16
- )
13
+ if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`)
17
14
  const tempObj = {}
18
15
  let arrayCounter = 0
19
- this.array.forEach((elem) => {
16
+ this.array.forEach(elem => {
20
17
  tempObj[elem] = array[arrayCounter]
21
18
  tempObj.toString = () => JSON.stringify(tempObj)
22
19
  arrayCounter++
@@ -26,13 +23,10 @@ class DataTable {
26
23
 
27
24
  /** @param {Array<*>} array */
28
25
  xadd(array) {
29
- if (array.length !== this.array.length)
30
- throw new Error(
31
- `There is too many elements in given data array. Please provide data in this format: ${this.array}`,
32
- )
26
+ if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`)
33
27
  const tempObj = {}
34
28
  let arrayCounter = 0
35
- this.array.forEach((elem) => {
29
+ this.array.forEach(elem => {
36
30
  tempObj[elem] = array[arrayCounter]
37
31
  tempObj.toString = () => JSON.stringify(tempObj)
38
32
  arrayCounter++
@@ -42,7 +36,7 @@ class DataTable {
42
36
 
43
37
  /** @param {Function} func */
44
38
  filter(func) {
45
- return this.rows.filter((row) => func(row.data))
39
+ return this.rows.filter(row => func(row.data))
46
40
  }
47
41
  }
48
42
 
package/lib/effects.js ADDED
@@ -0,0 +1,218 @@
1
+ const recorder = require('./recorder')
2
+ const { debug } = require('./output')
3
+ const store = require('./store')
4
+ const event = require('./event')
5
+
6
+ /**
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.
9
+ *
10
+ * @async
11
+ * @function hopeThat
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.
23
+ *
24
+ * @example
25
+ * const { hopeThat } = require('codeceptjs/effects')
26
+ * await hopeThat(() => {
27
+ * I.see('Welcome'); // Perform a soft assertion
28
+ * });
29
+ *
30
+ * @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
31
+ */
32
+ async function hopeThat(callback) {
33
+ if (store.dryRun) return
34
+ const sessionName = 'hopeThat'
35
+
36
+ let result = false
37
+ return recorder.add(
38
+ 'hopeThat',
39
+ () => {
40
+ recorder.session.start(sessionName)
41
+ store.hopeThat = true
42
+ callback()
43
+ recorder.add(() => {
44
+ result = true
45
+ recorder.session.restore(sessionName)
46
+ return result
47
+ })
48
+ recorder.session.catch(err => {
49
+ result = false
50
+ const msg = err.inspect ? err.inspect() : err.toString()
51
+ debug(`Unsuccessful assertion > ${msg}`)
52
+ event.dispatcher.once(event.test.finished, test => {
53
+ test.notes.push({ type: 'conditionalError', text: msg })
54
+ })
55
+ recorder.session.restore(sessionName)
56
+ return result
57
+ })
58
+ return recorder.add(
59
+ 'result',
60
+ () => {
61
+ store.hopeThat = undefined
62
+ return result
63
+ },
64
+ true,
65
+ false,
66
+ )
67
+ },
68
+ false,
69
+ false,
70
+ )
71
+ }
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
+
214
+ module.exports = {
215
+ hopeThat,
216
+ retryTo,
217
+ tryTo,
218
+ }
package/lib/els.js ADDED
@@ -0,0 +1,158 @@
1
+ const output = require('./output')
2
+ const store = require('./store')
3
+ const container = require('./container')
4
+ const StepConfig = require('./step/config')
5
+ const recordStep = require('./step/record')
6
+ const FuncStep = require('./step/func')
7
+ const { truth } = require('./assert/truth')
8
+ const { isAsyncFunction, humanizeFunction } = require('./utils')
9
+
10
+ function element(purpose, locator, fn) {
11
+ let stepConfig
12
+ if (arguments[arguments.length - 1] instanceof StepConfig) {
13
+ stepConfig = arguments[arguments.length - 1]
14
+ }
15
+
16
+ if (!fn || fn === stepConfig) {
17
+ fn = locator
18
+ locator = purpose
19
+ purpose = 'first element'
20
+ }
21
+
22
+ const step = prepareStep(purpose, locator, fn)
23
+ if (!step) return
24
+
25
+ return executeStep(
26
+ step,
27
+ async () => {
28
+ const els = await step.helper._locate(locator)
29
+ output.debug(`Found ${els.length} elements, using first element`)
30
+
31
+ return fn(els[0])
32
+ },
33
+ stepConfig,
34
+ )
35
+ }
36
+
37
+ function eachElement(purpose, locator, fn) {
38
+ if (!fn) {
39
+ fn = locator
40
+ locator = purpose
41
+ purpose = 'for each element'
42
+ }
43
+
44
+ const step = prepareStep(purpose, locator, fn)
45
+ if (!step) return
46
+
47
+ return executeStep(step, async () => {
48
+ const els = await step.helper._locate(locator)
49
+ output.debug(`Found ${els.length} elements for each elements to iterate`)
50
+
51
+ const errs = []
52
+ let i = 0
53
+ for (const el of els) {
54
+ try {
55
+ await fn(el, i)
56
+ } catch (err) {
57
+ output.error(`eachElement: failed operation on element #${i} ${el}`)
58
+ errs.push(err)
59
+ }
60
+ i++
61
+ }
62
+
63
+ if (errs.length) {
64
+ throw errs[0]
65
+ }
66
+ })
67
+ }
68
+
69
+ function expectElement(locator, fn) {
70
+ const step = prepareStep('expect element to be', locator, fn)
71
+ if (!step) return
72
+
73
+ return executeStep(step, async () => {
74
+ const els = await step.helper._locate(locator)
75
+ output.debug(`Found ${els.length} elements, first will be used for assertion`)
76
+
77
+ const result = await fn(els[0])
78
+ const assertion = truth(`element (${locator})`, fn.toString())
79
+ assertion.assert(result)
80
+ })
81
+ }
82
+
83
+ function expectAnyElement(locator, fn) {
84
+ const step = prepareStep('expect any element to be', locator, fn)
85
+ if (!step) return
86
+
87
+ return executeStep(step, async () => {
88
+ const els = await step.helper._locate(locator)
89
+ output.debug(`Found ${els.length} elements, at least one should pass the assertion`)
90
+
91
+ const assertion = truth(`any element of (${locator})`, fn.toString())
92
+
93
+ let found = false
94
+ for (const el of els) {
95
+ const result = await fn(el)
96
+ if (result) {
97
+ found = true
98
+ break
99
+ }
100
+ }
101
+ if (!found) throw assertion.getException()
102
+ })
103
+ }
104
+
105
+ function expectAllElements(locator, fn) {
106
+ const step = prepareStep('expect all elements', locator, fn)
107
+ if (!step) return
108
+
109
+ return executeStep(step, async () => {
110
+ const els = await step.helper._locate(locator)
111
+ output.debug(`Found ${els.length} elements, all should pass the assertion`)
112
+
113
+ let i = 1
114
+ for (const el of els) {
115
+ output.debug(`checking element #${i}: ${el}`)
116
+ const result = await fn(el)
117
+ const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
118
+ assertion.assert(result)
119
+ i++
120
+ }
121
+ })
122
+ }
123
+
124
+ module.exports = {
125
+ element,
126
+ eachElement,
127
+ expectElement,
128
+ expectAnyElement,
129
+ expectAllElements,
130
+ }
131
+
132
+ function prepareStep(purpose, locator, fn) {
133
+ if (store.dryRun) return
134
+ const helpers = Object.values(container.helpers())
135
+
136
+ const helper = helpers.filter(h => !!h._locate)[0]
137
+
138
+ if (!helper) {
139
+ throw new Error('No helper enabled with _locate method with returns a list of elements.')
140
+ }
141
+
142
+ if (!isAsyncFunction(fn)) {
143
+ throw new Error('Async function should be passed into each element')
144
+ }
145
+
146
+ const isAssertion = purpose.startsWith('expect')
147
+
148
+ const step = new FuncStep(`${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`)
149
+ step.setHelper(helper)
150
+ step.setArguments([humanizeFunction(fn)]) // user defined function is a passed argument
151
+
152
+ return step
153
+ }
154
+
155
+ async function executeStep(step, action, stepConfig = {}) {
156
+ step.setCallable(action)
157
+ return recordStep(step, [stepConfig])
158
+ }
package/lib/event.js CHANGED
@@ -1,10 +1,10 @@
1
- const debug = require('debug')('codeceptjs:event');
2
- const events = require('events');
3
- const { error } = require('./output');
1
+ const debug = require('debug')('codeceptjs:event')
2
+ const events = require('events')
3
+ const { error } = require('./output')
4
4
 
5
- const dispatcher = new events.EventEmitter();
5
+ const dispatcher = new events.EventEmitter()
6
6
 
7
- dispatcher.setMaxListeners(50);
7
+ dispatcher.setMaxListeners(50)
8
8
  /**
9
9
  * @namespace
10
10
  * @alias event
@@ -59,7 +59,9 @@ module.exports = {
59
59
  started: 'hook.start',
60
60
  passed: 'hook.passed',
61
61
  failed: 'hook.failed',
62
+ finished: 'hook.finished',
62
63
  },
64
+
63
65
  /**
64
66
  * @type {object}
65
67
  * @constant
@@ -140,33 +142,33 @@ module.exports = {
140
142
  * @param {*} [param]
141
143
  */
142
144
  emit(event, param) {
143
- let msg = `Emitted | ${event}`;
145
+ let msg = `Emitted | ${event}`
144
146
  if (param && param.toString()) {
145
- msg += ` (${param.toString()})`;
147
+ msg += ` (${param.toString()})`
146
148
  }
147
- debug(msg);
149
+ debug(msg)
148
150
  try {
149
- this.dispatcher.emit.apply(this.dispatcher, arguments);
151
+ this.dispatcher.emit.apply(this.dispatcher, arguments)
150
152
  } catch (err) {
151
- error(`Error processing ${event} event:`);
152
- error(err.stack);
153
+ error(`Error processing ${event} event:`)
154
+ error(err.stack)
153
155
  }
154
156
  },
155
157
 
156
158
  /** for testing only! */
157
159
  cleanDispatcher: () => {
158
- let event;
160
+ let event
159
161
  for (event in this.test) {
160
- this.dispatcher.removeAllListeners(this.test[event]);
162
+ this.dispatcher.removeAllListeners(this.test[event])
161
163
  }
162
164
  for (event in this.suite) {
163
- this.dispatcher.removeAllListeners(this.test[event]);
165
+ this.dispatcher.removeAllListeners(this.test[event])
164
166
  }
165
167
  for (event in this.step) {
166
- this.dispatcher.removeAllListeners(this.test[event]);
168
+ this.dispatcher.removeAllListeners(this.test[event])
167
169
  }
168
170
  for (event in this.all) {
169
- this.dispatcher.removeAllListeners(this.test[event]);
171
+ this.dispatcher.removeAllListeners(this.test[event])
170
172
  }
171
173
  },
172
- };
174
+ }