codeceptjs 3.7.0-beta.1 → 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 (73) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +7 -0
  3. package/lib/actor.js +46 -92
  4. package/lib/ai.js +130 -121
  5. package/lib/codecept.js +2 -2
  6. package/lib/command/check.js +186 -0
  7. package/lib/command/definitions.js +3 -1
  8. package/lib/command/interactive.js +1 -1
  9. package/lib/command/run-workers.js +2 -54
  10. package/lib/command/workers/runTests.js +64 -225
  11. package/lib/container.js +27 -0
  12. package/lib/effects.js +218 -0
  13. package/lib/els.js +87 -106
  14. package/lib/event.js +18 -17
  15. package/lib/heal.js +10 -0
  16. package/lib/helper/AI.js +2 -1
  17. package/lib/helper/Appium.js +31 -22
  18. package/lib/helper/Playwright.js +22 -1
  19. package/lib/helper/Puppeteer.js +5 -0
  20. package/lib/helper/WebDriver.js +29 -8
  21. package/lib/listener/emptyRun.js +2 -5
  22. package/lib/listener/exit.js +5 -8
  23. package/lib/listener/globalTimeout.js +66 -10
  24. package/lib/listener/result.js +12 -0
  25. package/lib/listener/steps.js +3 -6
  26. package/lib/listener/store.js +9 -1
  27. package/lib/mocha/asyncWrapper.js +15 -3
  28. package/lib/mocha/cli.js +79 -28
  29. package/lib/mocha/featureConfig.js +13 -0
  30. package/lib/mocha/hooks.js +32 -3
  31. package/lib/mocha/inject.js +5 -0
  32. package/lib/mocha/scenarioConfig.js +11 -0
  33. package/lib/mocha/suite.js +27 -1
  34. package/lib/mocha/test.js +102 -3
  35. package/lib/mocha/types.d.ts +11 -0
  36. package/lib/output.js +75 -73
  37. package/lib/pause.js +3 -10
  38. package/lib/plugin/analyze.js +349 -0
  39. package/lib/plugin/autoDelay.js +2 -2
  40. package/lib/plugin/commentStep.js +5 -0
  41. package/lib/plugin/customReporter.js +52 -0
  42. package/lib/plugin/heal.js +30 -0
  43. package/lib/plugin/pageInfo.js +140 -0
  44. package/lib/plugin/retryTo.js +18 -118
  45. package/lib/plugin/screenshotOnFail.js +12 -17
  46. package/lib/plugin/standardActingHelpers.js +4 -1
  47. package/lib/plugin/stepByStepReport.js +6 -5
  48. package/lib/plugin/stepTimeout.js +1 -1
  49. package/lib/plugin/tryTo.js +17 -107
  50. package/lib/recorder.js +5 -5
  51. package/lib/rerun.js +43 -42
  52. package/lib/result.js +161 -0
  53. package/lib/step/base.js +228 -0
  54. package/lib/step/config.js +50 -0
  55. package/lib/step/func.js +46 -0
  56. package/lib/step/helper.js +50 -0
  57. package/lib/step/meta.js +99 -0
  58. package/lib/step/record.js +74 -0
  59. package/lib/step/retry.js +11 -0
  60. package/lib/step/section.js +55 -0
  61. package/lib/step.js +20 -347
  62. package/lib/steps.js +50 -0
  63. package/lib/store.js +4 -0
  64. package/lib/timeout.js +66 -0
  65. package/lib/utils.js +93 -0
  66. package/lib/within.js +2 -2
  67. package/lib/workers.js +29 -49
  68. package/package.json +23 -20
  69. package/typings/index.d.ts +5 -4
  70. package/typings/promiseBasedTypes.d.ts +617 -7
  71. package/typings/types.d.ts +663 -34
  72. package/lib/listener/artifacts.js +0 -19
  73. package/lib/plugin/debugErrors.js +0 -67
package/lib/mocha/cli.js CHANGED
@@ -2,10 +2,11 @@ const {
2
2
  reporters: { Base },
3
3
  } = require('mocha')
4
4
  const ms = require('ms')
5
+ const figures = require('figures')
5
6
  const event = require('../event')
6
7
  const AssertionFailedError = require('../assert/error')
7
8
  const output = require('../output')
8
-
9
+ const { cloneTest } = require('./test')
9
10
  const cursor = Base.cursor
10
11
  let currentMetaStep = []
11
12
  let codeceptjsEventDispatchersRegistered = false
@@ -31,6 +32,16 @@ class Cli extends Base {
31
32
  output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`))
32
33
  }
33
34
 
35
+ if (level >= 3) {
36
+ process.on('warning', warning => {
37
+ console.log('\nWarning Details:')
38
+ console.log('Name:', warning.name)
39
+ console.log('Message:', warning.message)
40
+ console.log('Stack:', warning.stack)
41
+ console.log('-------------------')
42
+ })
43
+ }
44
+
34
45
  runner.on('start', () => {
35
46
  console.log()
36
47
  })
@@ -90,9 +101,11 @@ class Cli extends Base {
90
101
  event.dispatcher.on(event.step.started, step => {
91
102
  let processingStep = step
92
103
  const metaSteps = []
104
+ let isHidden = false
93
105
  while (processingStep.metaStep) {
94
106
  metaSteps.unshift(processingStep.metaStep)
95
107
  processingStep = processingStep.metaStep
108
+ if (processingStep.collapsed) isHidden = true
96
109
  }
97
110
  const shift = metaSteps.length
98
111
 
@@ -106,10 +119,9 @@ class Cli extends Base {
106
119
  }
107
120
  }
108
121
  currentMetaStep = metaSteps
122
+ if (isHidden) return
109
123
  output.stepShift = 3 + 2 * shift
110
- if (step.helper.constructor.name !== 'ExpectHelper') {
111
- output.step(step)
112
- }
124
+ output.step(step)
113
125
  })
114
126
 
115
127
  event.dispatcher.on(event.step.finished, () => {
@@ -137,16 +149,19 @@ class Cli extends Base {
137
149
  }
138
150
  }
139
151
 
140
- this.stats.pending += skippedCount
141
- this.stats.tests += skippedCount
152
+ const container = require('../container')
153
+ container.result().addStats({ pending: skippedCount, tests: skippedCount })
142
154
  })
143
155
 
144
156
  runner.on('end', this.result.bind(this))
145
157
  }
146
158
 
147
159
  result() {
148
- const stats = this.stats
149
- stats.failedHooks = 0
160
+ const container = require('../container')
161
+ container.result().addStats(this.stats)
162
+ container.result().finish()
163
+
164
+ const stats = container.result().stats
150
165
  console.log()
151
166
 
152
167
  // passes
@@ -159,53 +174,93 @@ class Cli extends Base {
159
174
  // failures
160
175
  if (stats.failures) {
161
176
  // append step traces
162
- this.failures.map(test => {
177
+ this.failures = this.failures.map(test => {
178
+ // we will change the stack trace, so we need to clone the test
163
179
  const err = test.err
164
180
 
165
181
  let log = ''
182
+ let originalMessage = err.message
166
183
 
167
184
  if (err instanceof AssertionFailedError) {
168
185
  err.message = err.inspect()
169
186
  }
170
187
 
188
+ // multi-line error messages (for Playwright)
189
+ if (err.message && err.message.includes('\n')) {
190
+ const lines = err.message.split('\n')
191
+ const truncatedLines = lines.slice(0, 5)
192
+ if (lines.length > 5) {
193
+ truncatedLines.push('...')
194
+ }
195
+ err.message = truncatedLines.join('\n').replace(/^/gm, ' ').trim()
196
+ }
197
+
198
+ // add new line before the message
199
+ err.message = '\n ' + err.message
200
+
201
+ // explicitly show file with error
202
+ if (test.file) {
203
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} ${output.styles.basic(test.file)}\n`
204
+ }
205
+
171
206
  const steps = test.steps || (test.ctx && test.ctx.test.steps)
172
207
 
173
208
  if (steps && steps.length) {
174
209
  let scenarioTrace = ''
175
- steps.reverse().forEach(step => {
176
- const line = `- ${step.toCode()} ${step.line()}`
177
- // if (step.status === 'failed') line = '' + line;
178
- scenarioTrace += `\n${line}`
179
- })
180
- log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`
210
+ steps
211
+ .reverse()
212
+ .slice(0, 10)
213
+ .forEach(step => {
214
+ const hasFailed = step.status === 'failed'
215
+ let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
216
+ if (hasFailed) line = output.styles.bold(line)
217
+ scenarioTrace += `\n${line}`
218
+ })
219
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
181
220
  }
182
221
 
183
222
  // display artifacts in debug mode
184
223
  if (test?.artifacts && Object.keys(test.artifacts).length) {
185
- log += `\n${output.styles.bold('Artifacts:')}`
224
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Artifacts:')}`
186
225
  for (const artifact of Object.keys(test.artifacts)) {
187
226
  log += `\n- ${artifact}: ${test.artifacts[artifact]}`
188
227
  }
189
228
  }
190
229
 
230
+ // display metadata
231
+ if (test.meta && Object.keys(test.meta).length) {
232
+ log += `\n\n${output.styles.basic(figures.circle)} ${output.styles.section('Metadata:')}`
233
+ for (const [key, value] of Object.entries(test.meta)) {
234
+ log += `\n- ${key}: ${value}`
235
+ }
236
+ }
237
+
191
238
  try {
192
- let stack = err.stack ? err.stack.split('\n') : []
239
+ let stack = err.stack
240
+ stack = (stack || '').replace(originalMessage, '')
241
+ stack = stack ? stack.split('\n') : []
242
+
193
243
  if (stack[0] && stack[0].includes(err.message)) {
194
244
  stack.shift()
195
245
  }
196
246
 
247
+ if (stack[0] && stack[0].trim() == 'Error:') {
248
+ stack.shift()
249
+ }
250
+
197
251
  if (output.level() < 3) {
198
252
  stack = stack.slice(0, 3)
199
253
  }
200
254
 
201
255
  err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`
202
-
203
- // clone err object so stack trace adjustments won't affect test other reports
204
- test.err = err
205
- return test
206
256
  } catch (e) {
207
- throw Error(e)
257
+ console.error(e)
208
258
  }
259
+
260
+ // we will change the stack trace, so we need to clone the test
261
+ test = cloneTest(test)
262
+ test.err = err
263
+ return test
209
264
  })
210
265
 
211
266
  const originalLog = Base.consoleLog
@@ -218,12 +273,8 @@ class Cli extends Base {
218
273
  console.log()
219
274
  }
220
275
 
221
- this.failures.forEach(failure => {
222
- if (failure.constructor.name === 'Hook') {
223
- stats.failedHooks += 1
224
- }
225
- })
226
- event.emit(event.all.failures, { failuresLog, stats })
276
+ container.result().addFailures(failuresLog)
277
+
227
278
  output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks)
228
279
 
229
280
  if (stats.failures && output.level() < 3) {
@@ -7,6 +7,19 @@ class FeatureConfig {
7
7
  this.suite = suite
8
8
  }
9
9
 
10
+ /**
11
+ * Set metadata for this suite
12
+ * @param {string} key
13
+ * @param {string} value
14
+ * @returns {this}
15
+ */
16
+ meta(key, value) {
17
+ this.suite.tests.forEach(test => {
18
+ test.meta[key] = value
19
+ })
20
+ return this
21
+ }
22
+
10
23
  /**
11
24
  * Retry this test for number of times
12
25
  *
@@ -1,18 +1,47 @@
1
1
  const event = require('../event')
2
+ const { serializeError } = require('../utils')
3
+ // const { serializeTest } = require('./test')
2
4
 
5
+ /**
6
+ * Represents a test hook in the testing framework
7
+ * @class
8
+ * @property {Object} suite - The test suite this hook belongs to
9
+ * @property {Object} test - The test object associated with this hook
10
+ * @property {Object} runnable - The current test being executed
11
+ * @property {Object} ctx - The context object
12
+ * @property {Error|null} err - The error that occurred during hook execution, if any
13
+ */
3
14
  class Hook {
15
+ /**
16
+ * Creates a new Hook instance
17
+ * @param {Object} context - The context object containing suite and test information
18
+ * @param {Object} context.suite - The test suite
19
+ * @param {Object} context.test - The test object
20
+ * @param {Object} context.ctx - The context object
21
+ * @param {Error} error - The error object if hook execution failed
22
+ */
4
23
  constructor(context, error) {
5
24
  this.suite = context.suite
6
25
  this.test = context.test
7
26
  this.runnable = context?.ctx?.test
8
27
  this.ctx = context.ctx
9
- this.error = error
28
+ this.err = error
10
29
  }
11
30
 
12
31
  get hookName() {
13
32
  return this.constructor.name.replace('Hook', '')
14
33
  }
15
34
 
35
+ simplify() {
36
+ return {
37
+ hookName: this.hookName,
38
+ title: this.title,
39
+ // test: this.test ? serializeTest(this.test) : null,
40
+ // suite: this.suite ? serializeSuite(this.suite) : null,
41
+ error: this.err ? serializeError(this.err) : null,
42
+ }
43
+ }
44
+
16
45
  toString() {
17
46
  return this.hookName
18
47
  }
@@ -46,13 +75,13 @@ function fireHook(eventType, suite, error) {
46
75
  const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1]
47
76
  switch (hook) {
48
77
  case 'before each':
49
- event.emit(eventType, new BeforeHook(suite))
78
+ event.emit(eventType, new BeforeHook(suite, error))
50
79
  break
51
80
  case 'after each':
52
81
  event.emit(eventType, new AfterHook(suite, error))
53
82
  break
54
83
  case 'before all':
55
- event.emit(eventType, new BeforeSuiteHook(suite))
84
+ event.emit(eventType, new BeforeSuiteHook(suite, error))
56
85
  break
57
86
  case 'after all':
58
87
  event.emit(eventType, new AfterSuiteHook(suite, error))
@@ -5,6 +5,7 @@ const getInjectedArguments = (fn, test) => {
5
5
  const testArgs = {}
6
6
  const params = parser.getParams(fn) || []
7
7
  const objects = container.support()
8
+
8
9
  for (const key of params) {
9
10
  testArgs[key] = {}
10
11
  if (test && test.inject && test.inject[key]) {
@@ -18,6 +19,10 @@ const getInjectedArguments = (fn, test) => {
18
19
  testArgs[key] = container.support(key)
19
20
  }
20
21
 
22
+ if (test) {
23
+ testArgs.suite = test?.parent
24
+ testArgs.test = test
25
+ }
21
26
  return testArgs
22
27
  }
23
28
 
@@ -42,6 +42,17 @@ class ScenarioConfig {
42
42
  return this
43
43
  }
44
44
 
45
+ /**
46
+ * Set metadata for this test
47
+ * @param {string} key
48
+ * @param {string} value
49
+ * @returns {this}
50
+ */
51
+ meta(key, value) {
52
+ this.test.meta[key] = value
53
+ return this
54
+ }
55
+
45
56
  /**
46
57
  * Set timeout for this test
47
58
  * @param {number} timeout
@@ -1,5 +1,4 @@
1
1
  const MochaSuite = require('mocha/lib/suite')
2
-
3
2
  /**
4
3
  * @typedef {import('mocha')} Mocha
5
4
  */
@@ -34,6 +33,10 @@ function enhanceMochaSuite(suite) {
34
33
  }
35
34
  }
36
35
 
36
+ suite.simplify = function () {
37
+ return serializeSuite(this)
38
+ }
39
+
37
40
  return suite
38
41
  }
39
42
 
@@ -49,7 +52,30 @@ function createSuite(parent, title) {
49
52
  return enhanceMochaSuite(suite)
50
53
  }
51
54
 
55
+ function serializeSuite(suite) {
56
+ suite = { ...suite }
57
+
58
+ return {
59
+ opts: suite.opts || {},
60
+ tags: suite.tags || [],
61
+ retries: suite._retries,
62
+ title: suite.title,
63
+ status: suite.status,
64
+ notes: suite.notes || [],
65
+ meta: suite.meta || {},
66
+ duration: suite.duration || 0,
67
+ }
68
+ }
69
+
70
+ function deserializeSuite(suite) {
71
+ suite = Object.assign(new MochaSuite(suite.title), suite)
72
+ enhanceMochaSuite(suite)
73
+ return suite
74
+ }
75
+
52
76
  module.exports = {
53
77
  createSuite,
54
78
  enhanceMochaSuite,
79
+ serializeSuite,
80
+ deserializeSuite,
55
81
  }
package/lib/mocha/test.js CHANGED
@@ -1,8 +1,9 @@
1
1
  const Test = require('mocha/lib/test')
2
+ const Suite = require('mocha/lib/suite')
2
3
  const { test: testWrapper } = require('./asyncWrapper')
3
- const { enhanceMochaSuite } = require('./suite')
4
- const { genTestId } = require('../utils')
5
-
4
+ const { enhanceMochaSuite, createSuite } = require('./suite')
5
+ const { genTestId, serializeError, clearString, relativeDir } = require('../utils')
6
+ const Step = require('../step/base')
6
7
  /**
7
8
  * Factory function to create enhanced tests
8
9
  * @param {string} title - Test title
@@ -31,6 +32,12 @@ function enhanceMochaTest(test) {
31
32
  test.artifacts = []
32
33
  test.inject = {}
33
34
  test.opts = {}
35
+ test.meta = {}
36
+
37
+ test.notes = []
38
+ test.addNote = (type, note) => {
39
+ test.notes.push({ type, text: note })
40
+ }
34
41
 
35
42
  // Add new methods
36
43
  /**
@@ -39,6 +46,7 @@ function enhanceMochaTest(test) {
39
46
  test.addToSuite = function (suite) {
40
47
  enhanceMochaSuite(suite)
41
48
  suite.addTest(testWrapper(this))
49
+ if (test.file && !suite.file) suite.file = test.file
42
50
  test.tags = [...(test.tags || []), ...(suite.tags || [])]
43
51
  test.fullTitle = () => `${suite.title}: ${test.title}`
44
52
  test.uid = genTestId(test)
@@ -47,14 +55,105 @@ function enhanceMochaTest(test) {
47
55
  test.applyOptions = function (opts) {
48
56
  if (!opts) opts = {}
49
57
  test.opts = opts
58
+ test.meta = opts.meta || {}
50
59
  test.totalTimeout = opts.timeout
51
60
  if (opts.retries) this.retries(opts.retries)
52
61
  }
53
62
 
63
+ test.simplify = function () {
64
+ return serializeTest(this)
65
+ }
66
+
67
+ return test
68
+ }
69
+
70
+ function deserializeTest(test) {
71
+ test = Object.assign(
72
+ createTest(test.title || '', () => {}),
73
+ test,
74
+ )
75
+ test.parent = Object.assign(new Suite(test.parent?.title || 'Suite'), test.parent)
76
+ enhanceMochaSuite(test.parent)
77
+ if (test.steps) test.steps = test.steps.map(step => Object.assign(new Step(step.title), step))
54
78
  return test
55
79
  }
56
80
 
81
+ function serializeTest(test, error = null) {
82
+ // test = { ...test }
83
+
84
+ if (test.start && !test.duration) {
85
+ const end = +new Date()
86
+ test.duration = end - test.start
87
+ }
88
+
89
+ let err
90
+
91
+ if (test.err) {
92
+ err = serializeError(test.err)
93
+ test.state = 'failed'
94
+ } else if (error) {
95
+ err = serializeError(error)
96
+ test.state = 'failed'
97
+ }
98
+ const parent = {}
99
+ if (test.parent) {
100
+ parent.title = test.parent.title
101
+ }
102
+
103
+ if (test.opts) {
104
+ Object.keys(test.opts).forEach(k => {
105
+ if (typeof test.opts[k] === 'object') delete test.opts[k]
106
+ if (typeof test.opts[k] === 'function') delete test.opts[k]
107
+ })
108
+ }
109
+
110
+ let steps = undefined
111
+ if (Array.isArray(test.steps)) {
112
+ steps = test.steps.map(step => (step.simplify ? step.simplify() : step))
113
+ }
114
+
115
+ return {
116
+ opts: test.opts || {},
117
+ tags: test.tags || [],
118
+ uid: test.uid,
119
+ retries: test._retries,
120
+ title: test.title,
121
+ state: test.state,
122
+ notes: test.notes || [],
123
+ meta: test.meta || {},
124
+ artifacts: test.artifacts || {},
125
+ duration: test.duration || 0,
126
+ err,
127
+ parent,
128
+ steps,
129
+ }
130
+ }
131
+
132
+ function cloneTest(test) {
133
+ return deserializeTest(serializeTest(test))
134
+ }
135
+
136
+ function testToFileName(test) {
137
+ let fileName = clearString(test.title)
138
+ // remove tags with empty string (disable for now)
139
+ // fileName = fileName.replace(/\@\w+/g, '')
140
+ fileName = fileName.slice(0, 100)
141
+ if (fileName.indexOf('{') !== -1) {
142
+ fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
143
+ }
144
+ if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
145
+ // TODO: add suite title to file name
146
+ // if (test.parent && test.parent.title) {
147
+ // fileName = `${clearString(test.parent.title)}_${fileName}`
148
+ // }
149
+ return fileName
150
+ }
151
+
57
152
  module.exports = {
58
153
  createTest,
154
+ testToFileName,
59
155
  enhanceMochaTest,
156
+ serializeTest,
157
+ deserializeTest,
158
+ cloneTest,
60
159
  }
@@ -7,14 +7,25 @@ declare global {
7
7
  title: string
8
8
  tags: string[]
9
9
  steps: string[]
10
+ meta: Record<string, any>
11
+ notes: Array<{
12
+ type: string
13
+ text: string
14
+ }>
15
+ state: string
16
+ err?: Error
10
17
  config: Record<string, any>
11
18
  artifacts: string[]
12
19
  inject: Record<string, any>
13
20
  opts: Record<string, any>
14
21
  throws?: Error | string | RegExp | Function
15
22
  totalTimeout?: number
23
+ relativeFile?: string
16
24
  addToSuite(suite: Mocha.Suite): void
17
25
  applyOptions(opts: Record<string, any>): void
26
+ simplify(): Record<string, any>
27
+ toFileName(): string
28
+ addNote(type: string, note: string): void
18
29
  codeceptjs: boolean
19
30
  }
20
31