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
@@ -1,11 +1,27 @@
1
- /** @class */
1
+ /**
2
+ * Configuration for a Feature.
3
+ * Can inject values and add custom configuration.
4
+ */
2
5
  class FeatureConfig {
3
6
  constructor(suite) {
4
7
  this.suite = suite
5
8
  }
6
9
 
7
10
  /**
8
- * Retry this suite for x times
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
+
23
+ /**
24
+ * Retry this test for number of times
9
25
  *
10
26
  * @param {number} retries
11
27
  * @returns {this}
@@ -16,15 +32,11 @@ class FeatureConfig {
16
32
  }
17
33
 
18
34
  /**
19
- * Set timeout for this suite
35
+ * Set timeout for this test
20
36
  * @param {number} timeout
21
37
  * @returns {this}
22
- * @deprecated
23
38
  */
24
39
  timeout(timeout) {
25
- console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`)
26
- console.log(`Please use Feature('${this.suite.title}', { timeout: ${timeout / 1000} }) instead`)
27
- console.log('Timeout should be set in seconds')
28
40
  this.suite.timeout(timeout)
29
41
  return this
30
42
  }
@@ -32,8 +44,9 @@ class FeatureConfig {
32
44
  /**
33
45
  * Configures a helper.
34
46
  * Helper name can be omitted and values will be applied to first helper.
35
- * @param {string | Object<string, *>} helper
36
- * @param {Object<string, *>} [obj]
47
+ *
48
+ * @param {string|number} helper
49
+ * @param {*} obj
37
50
  * @returns {this}
38
51
  */
39
52
  config(helper, obj) {
@@ -57,9 +70,8 @@ class FeatureConfig {
57
70
  * @returns {this}
58
71
  */
59
72
  tag(tagName) {
60
- if (tagName[0] !== '@') {
61
- tagName = `@${tagName}`
62
- }
73
+ if (tagName[0] !== '@') tagName = `@${tagName}`
74
+ if (!this.suite.tags) this.suite.tags = []
63
75
  this.suite.tags.push(tagName)
64
76
  this.suite.title = `${this.suite.title.trim()} ${tagName}`
65
77
  return this
@@ -1,11 +1,13 @@
1
1
  const Gherkin = require('@cucumber/gherkin')
2
2
  const Messages = require('@cucumber/messages')
3
- const { Context, Suite, Test } = require('mocha')
3
+ const { Context, Suite } = require('mocha')
4
4
  const debug = require('debug')('codeceptjs:bdd')
5
5
 
6
+ const { enhanceMochaSuite } = require('./suite')
7
+ const { createTest } = require('./test')
6
8
  const { matchStep } = require('./bdd')
7
9
  const event = require('../event')
8
- const scenario = require('../scenario')
10
+ const { injected, setup, teardown, suiteSetup, suiteTeardown } = require('./asyncWrapper')
9
11
  const Step = require('../step')
10
12
  const DataTableArgument = require('../data/dataTableArgument')
11
13
  const transform = require('../transform')
@@ -28,7 +30,8 @@ module.exports = (text, file) => {
28
30
  throw new Error(`No 'Features' available in Gherkin '${file}' provided!`)
29
31
  }
30
32
  const suite = new Suite(ast.feature.name, new Context())
31
- const tags = ast.feature.tags.map((t) => t.name)
33
+ enhanceMochaSuite(suite)
34
+ const tags = ast.feature.tags.map(t => t.name)
32
35
  suite.title = `${suite.title} ${tags.join(' ')}`.trim()
33
36
  suite.tags = tags || []
34
37
  suite.comment = ast.feature.description
@@ -36,17 +39,17 @@ module.exports = (text, file) => {
36
39
  suite.file = file
37
40
  suite.timeout(0)
38
41
 
39
- suite.beforeEach('codeceptjs.before', () => scenario.setup(suite))
40
- suite.afterEach('codeceptjs.after', () => scenario.teardown(suite))
41
- suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite))
42
- suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite))
42
+ suite.beforeEach('codeceptjs.before', () => setup(suite))
43
+ suite.afterEach('codeceptjs.after', () => teardown(suite))
44
+ suite.beforeAll('codeceptjs.beforeSuite', () => suiteSetup(suite))
45
+ suite.afterAll('codeceptjs.afterSuite', () => suiteTeardown(suite))
43
46
 
44
- const runSteps = async (steps) => {
47
+ const runSteps = async steps => {
45
48
  for (const step of steps) {
46
49
  const metaStep = new Step.MetaStep(null, step.text)
47
50
  metaStep.actor = step.keyword.trim()
48
51
  let helperStep
49
- const setMetaStep = (step) => {
52
+ const setMetaStep = step => {
50
53
  helperStep = step
51
54
  if (step.metaStep) {
52
55
  if (step.metaStep === metaStep) {
@@ -100,18 +103,13 @@ module.exports = (text, file) => {
100
103
  if (child.background) {
101
104
  suite.beforeEach(
102
105
  'Before',
103
- scenario.injected(async () => runSteps(child.background.steps), suite, 'before'),
106
+ injected(async () => runSteps(child.background.steps), suite, 'before'),
104
107
  )
105
108
  continue
106
109
  }
107
- if (
108
- child.scenario &&
109
- (currentLanguage
110
- ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline
111
- : child.scenario.keyword === 'Scenario Outline')
112
- ) {
110
+ if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) {
113
111
  for (const examples of child.scenario.examples) {
114
- const fields = examples.tableHeader.cells.map((c) => c.value)
112
+ const fields = examples.tableHeader.cells.map(c => c.value)
115
113
  for (const example of examples.tableBody) {
116
114
  let exampleSteps = [...child.scenario.steps]
117
115
  const current = {}
@@ -120,13 +118,13 @@ module.exports = (text, file) => {
120
118
  const value = transform('gherkin.examples', example.cells[index].value)
121
119
  example.cells[index].value = value
122
120
  current[placeholder] = value
123
- exampleSteps = exampleSteps.map((step) => {
121
+ exampleSteps = exampleSteps.map(step => {
124
122
  step = { ...step }
125
123
  step.text = step.text.split(`<${placeholder}>`).join(value)
126
124
  return step
127
125
  })
128
126
  }
129
- const tags = child.scenario.tags.map((t) => t.name).concat(examples.tags.map((t) => t.name))
127
+ const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name))
130
128
  let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim()
131
129
 
132
130
  for (const [key, value] of Object.entries(current)) {
@@ -135,22 +133,22 @@ module.exports = (text, file) => {
135
133
  }
136
134
  }
137
135
 
138
- const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)))
136
+ const test = createTest(title, async () => runSteps(addExampleInTable(exampleSteps, current)))
137
+ test.addToSuite(suite)
139
138
  test.tags = suite.tags.concat(tags)
140
139
  test.file = file
141
- suite.addTest(scenario.test(test))
142
140
  }
143
141
  }
144
142
  continue
145
143
  }
146
144
 
147
145
  if (child.scenario) {
148
- const tags = child.scenario.tags.map((t) => t.name)
146
+ const tags = child.scenario.tags.map(t => t.name)
149
147
  const title = `${child.scenario.name} ${tags.join(' ')}`.trim()
150
- const test = new Test(title, async () => runSteps(child.scenario.steps))
148
+ const test = createTest(title, async () => runSteps(child.scenario.steps))
149
+ test.addToSuite(suite)
151
150
  test.tags = suite.tags.concat(tags)
152
151
  test.file = file
153
- suite.addTest(scenario.test(test))
154
152
  }
155
153
  }
156
154
 
@@ -162,8 +160,8 @@ function transformTable(table) {
162
160
  for (const id in table.rows) {
163
161
  const cells = table.rows[id].cells
164
162
  str += cells
165
- .map((c) => c.value)
166
- .map((c) => c.padEnd(15))
163
+ .map(c => c.value)
164
+ .map(c => c.padEnd(15))
167
165
  .join(' | ')
168
166
  str += '\n'
169
167
  }
@@ -172,12 +170,12 @@ function transformTable(table) {
172
170
  function addExampleInTable(exampleSteps, placeholders) {
173
171
  const steps = JSON.parse(JSON.stringify(exampleSteps))
174
172
  for (const placeholder in placeholders) {
175
- steps.map((step) => {
173
+ steps.map(step => {
176
174
  step = { ...step }
177
175
  if (step.dataTable) {
178
176
  for (const id in step.dataTable.rows) {
179
177
  const cells = step.dataTable.rows[id].cells
180
- cells.map((c) => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])))
178
+ cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])))
181
179
  }
182
180
  }
183
181
  return step
@@ -0,0 +1,112 @@
1
+ const event = require('../event')
2
+ const { serializeError } = require('../utils')
3
+ // const { serializeTest } = require('./test')
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
+ */
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
+ */
23
+ constructor(context, error) {
24
+ this.suite = context.suite
25
+ this.test = context.test
26
+ this.runnable = context?.ctx?.test
27
+ this.ctx = context.ctx
28
+ this.err = error
29
+ }
30
+
31
+ get hookName() {
32
+ return this.constructor.name.replace('Hook', '')
33
+ }
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
+
45
+ toString() {
46
+ return this.hookName
47
+ }
48
+
49
+ toCode() {
50
+ return this.toString() + '()'
51
+ }
52
+
53
+ retry(n) {
54
+ this.suite.opts[`retry${this.hookName}`] = n
55
+ }
56
+
57
+ get title() {
58
+ return this.ctx?.test?.title || this.name
59
+ }
60
+
61
+ get name() {
62
+ return this.constructor.name
63
+ }
64
+ }
65
+
66
+ class BeforeHook extends Hook {}
67
+
68
+ class AfterHook extends Hook {}
69
+
70
+ class BeforeSuiteHook extends Hook {}
71
+
72
+ class AfterSuiteHook extends Hook {}
73
+
74
+ function fireHook(eventType, suite, error) {
75
+ const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1]
76
+ switch (hook) {
77
+ case 'before each':
78
+ event.emit(eventType, new BeforeHook(suite, error))
79
+ break
80
+ case 'after each':
81
+ event.emit(eventType, new AfterHook(suite, error))
82
+ break
83
+ case 'before all':
84
+ event.emit(eventType, new BeforeSuiteHook(suite, error))
85
+ break
86
+ case 'after all':
87
+ event.emit(eventType, new AfterSuiteHook(suite, error))
88
+ break
89
+ default:
90
+ event.emit(eventType, suite, error)
91
+ }
92
+ }
93
+
94
+ class HookConfig {
95
+ constructor(hook) {
96
+ this.hook = hook
97
+ }
98
+
99
+ retry(n) {
100
+ this.hook.retry(n)
101
+ return this
102
+ }
103
+ }
104
+
105
+ module.exports = {
106
+ BeforeHook,
107
+ AfterHook,
108
+ BeforeSuiteHook,
109
+ AfterSuiteHook,
110
+ fireHook,
111
+ HookConfig,
112
+ }
@@ -0,0 +1,12 @@
1
+ const Suite = require('mocha/lib/suite')
2
+ const Test = require('mocha/lib/test')
3
+ const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('./hooks')
4
+
5
+ module.exports = {
6
+ Suite,
7
+ Test,
8
+ BeforeHook,
9
+ AfterHook,
10
+ BeforeSuiteHook,
11
+ AfterSuiteHook,
12
+ }
@@ -0,0 +1,29 @@
1
+ const parser = require('../parser')
2
+
3
+ const getInjectedArguments = (fn, test) => {
4
+ const container = require('../container')
5
+ const testArgs = {}
6
+ const params = parser.getParams(fn) || []
7
+ const objects = container.support()
8
+
9
+ for (const key of params) {
10
+ testArgs[key] = {}
11
+ if (test && test.inject && test.inject[key]) {
12
+ // @FIX: need fix got inject
13
+ testArgs[key] = test.inject[key]
14
+ continue
15
+ }
16
+ if (!objects[key]) {
17
+ throw new Error(`Object of type ${key} is not defined in container`)
18
+ }
19
+ testArgs[key] = container.support(key)
20
+ }
21
+
22
+ if (test) {
23
+ testArgs.suite = test?.parent
24
+ testArgs.test = test
25
+ }
26
+ return testArgs
27
+ }
28
+
29
+ module.exports.getInjectedArguments = getInjectedArguments
@@ -1,3 +1,5 @@
1
+ const { isAsyncFunction } = require('../utils')
2
+
1
3
  /** @class */
2
4
  class ScenarioConfig {
3
5
  constructor(test) {
@@ -40,6 +42,17 @@ class ScenarioConfig {
40
42
  return this
41
43
  }
42
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
+
43
56
  /**
44
57
  * Set timeout for this test
45
58
  * @param {number} timeout
@@ -71,17 +84,19 @@ class ScenarioConfig {
71
84
  * @param {Object<string, any>} [obj]
72
85
  * @returns {this}
73
86
  */
74
- async config(helper, obj) {
87
+ config(helper, obj) {
75
88
  if (!obj) {
76
89
  obj = helper
77
90
  helper = 0
78
91
  }
79
92
  if (typeof obj === 'function') {
80
- obj = await obj(this.test)
81
- }
82
- if (!this.test.config) {
83
- this.test.config = {}
93
+ if (isAsyncFunction(obj)) {
94
+ obj(this.test).then(res => (this.test.config[helper] = res))
95
+ return this
96
+ }
97
+ obj = obj(this.test)
84
98
  }
99
+
85
100
  this.test.config[helper] = obj
86
101
  return this
87
102
  }
@@ -104,7 +119,7 @@ class ScenarioConfig {
104
119
  * @returns {this}
105
120
  */
106
121
  injectDependencies(dependencies) {
107
- Object.keys(dependencies).forEach((key) => {
122
+ Object.keys(dependencies).forEach(key => {
108
123
  this.test.inject[key] = dependencies[key]
109
124
  })
110
125
  return this
@@ -0,0 +1,81 @@
1
+ const MochaSuite = require('mocha/lib/suite')
2
+ /**
3
+ * @typedef {import('mocha')} Mocha
4
+ */
5
+
6
+ /**
7
+ * Enhances MochaSuite with CodeceptJS specific functionality using composition
8
+ */
9
+ function enhanceMochaSuite(suite) {
10
+ // already enhanced
11
+ if (suite.codeceptjs) return suite
12
+
13
+ suite.codeceptjs = true
14
+ // Add properties
15
+ suite.tags = suite.title.match(/(\@[a-zA-Z0-9-_]+)/g) || []
16
+ suite.opts = {}
17
+ // suite.totalTimeout = undefined
18
+
19
+ // Override fullTitle method
20
+ suite.fullTitle = () => `${suite.title}:`
21
+
22
+ // Add new methods
23
+ suite.applyOptions = function (opts) {
24
+ if (!opts) opts = {}
25
+ suite.opts = opts
26
+
27
+ if (opts.retries) suite.retries(opts.retries)
28
+ if (opts.timeout) suite.totalTimeout = opts.timeout
29
+
30
+ if (opts.skipInfo && opts.skipInfo.skipped) {
31
+ suite.pending = true
32
+ suite.opts = { ...this.opts, skipInfo: opts.skipInfo }
33
+ }
34
+ }
35
+
36
+ suite.simplify = function () {
37
+ return serializeSuite(this)
38
+ }
39
+
40
+ return suite
41
+ }
42
+
43
+ /**
44
+ * Factory function to create enhanced suites
45
+ * @param {Mocha.Suite} parent - Parent suite
46
+ * @param {string} title - Suite title
47
+ * @returns {CodeceptJS.Suite & Mocha.Suite} New enhanced suite instance
48
+ */
49
+ function createSuite(parent, title) {
50
+ const suite = MochaSuite.create(parent, title)
51
+ suite.timeout(0)
52
+ return enhanceMochaSuite(suite)
53
+ }
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
+
76
+ module.exports = {
77
+ createSuite,
78
+ enhanceMochaSuite,
79
+ serializeSuite,
80
+ deserializeSuite,
81
+ }
@@ -0,0 +1,159 @@
1
+ const Test = require('mocha/lib/test')
2
+ const Suite = require('mocha/lib/suite')
3
+ const { test: testWrapper } = require('./asyncWrapper')
4
+ const { enhanceMochaSuite, createSuite } = require('./suite')
5
+ const { genTestId, serializeError, clearString, relativeDir } = require('../utils')
6
+ const Step = require('../step/base')
7
+ /**
8
+ * Factory function to create enhanced tests
9
+ * @param {string} title - Test title
10
+ * @param {Function} fn - Test function
11
+ * @returns {CodeceptJS.Test & Mocha.Test} New enhanced test instance
12
+ */
13
+ function createTest(title, fn) {
14
+ const test = new Test(title, fn)
15
+ return enhanceMochaTest(test)
16
+ }
17
+
18
+ /**
19
+ * Enhances Mocha Test with CodeceptJS specific functionality using composition
20
+ * @param {CodeceptJS.Test & Mocha.Test} test - Test instance to enhance
21
+ * @returns {CodeceptJS.Test & Mocha.Test} Enhanced test instance
22
+ */
23
+ function enhanceMochaTest(test) {
24
+ // already enhanced
25
+ if (test.codeceptjs) return test
26
+
27
+ test.codeceptjs = true
28
+ // Add properties
29
+ test.tags = test.title.match(/(\@[a-zA-Z0-9-_]+)/g) || []
30
+ test.steps = []
31
+ test.config = {}
32
+ test.artifacts = []
33
+ test.inject = {}
34
+ test.opts = {}
35
+ test.meta = {}
36
+
37
+ test.notes = []
38
+ test.addNote = (type, note) => {
39
+ test.notes.push({ type, text: note })
40
+ }
41
+
42
+ // Add new methods
43
+ /**
44
+ * @param {Mocha.Suite} suite - The Mocha suite to add this test to
45
+ */
46
+ test.addToSuite = function (suite) {
47
+ enhanceMochaSuite(suite)
48
+ suite.addTest(testWrapper(this))
49
+ if (test.file && !suite.file) suite.file = test.file
50
+ test.tags = [...(test.tags || []), ...(suite.tags || [])]
51
+ test.fullTitle = () => `${suite.title}: ${test.title}`
52
+ test.uid = genTestId(test)
53
+ }
54
+
55
+ test.applyOptions = function (opts) {
56
+ if (!opts) opts = {}
57
+ test.opts = opts
58
+ test.meta = opts.meta || {}
59
+ test.totalTimeout = opts.timeout
60
+ if (opts.retries) this.retries(opts.retries)
61
+ }
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))
78
+ return test
79
+ }
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
+
152
+ module.exports = {
153
+ createTest,
154
+ testToFileName,
155
+ enhanceMochaTest,
156
+ serializeTest,
157
+ deserializeTest,
158
+ cloneTest,
159
+ }