codeceptjs 4.0.0-beta.4 → 4.0.0-beta.6.esm-aria

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 (188) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +53 -54
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +70 -102
  5. package/lib/ai.js +131 -121
  6. package/lib/assert/empty.js +11 -12
  7. package/lib/assert/equal.js +16 -21
  8. package/lib/assert/error.js +2 -2
  9. package/lib/assert/include.js +11 -15
  10. package/lib/assert/throws.js +3 -5
  11. package/lib/assert/truth.js +10 -7
  12. package/lib/assert.js +18 -18
  13. package/lib/codecept.js +112 -101
  14. package/lib/colorUtils.js +48 -50
  15. package/lib/command/check.js +206 -0
  16. package/lib/command/configMigrate.js +13 -14
  17. package/lib/command/definitions.js +24 -36
  18. package/lib/command/dryRun.js +16 -16
  19. package/lib/command/generate.js +38 -39
  20. package/lib/command/gherkin/init.js +36 -38
  21. package/lib/command/gherkin/snippets.js +76 -74
  22. package/lib/command/gherkin/steps.js +21 -18
  23. package/lib/command/info.js +49 -15
  24. package/lib/command/init.js +41 -37
  25. package/lib/command/interactive.js +22 -13
  26. package/lib/command/list.js +11 -10
  27. package/lib/command/run-multiple/chunk.js +50 -47
  28. package/lib/command/run-multiple/collection.js +5 -5
  29. package/lib/command/run-multiple/run.js +3 -3
  30. package/lib/command/run-multiple.js +27 -47
  31. package/lib/command/run-rerun.js +6 -7
  32. package/lib/command/run-workers.js +15 -66
  33. package/lib/command/run.js +8 -8
  34. package/lib/command/utils.js +22 -21
  35. package/lib/command/workers/runTests.js +131 -241
  36. package/lib/config.js +111 -49
  37. package/lib/container.js +589 -244
  38. package/lib/data/context.js +16 -18
  39. package/lib/data/dataScenarioConfig.js +9 -9
  40. package/lib/data/dataTableArgument.js +7 -7
  41. package/lib/data/table.js +6 -12
  42. package/lib/effects.js +307 -0
  43. package/lib/els.js +160 -0
  44. package/lib/event.js +24 -19
  45. package/lib/globals.js +141 -0
  46. package/lib/heal.js +89 -81
  47. package/lib/helper/AI.js +3 -2
  48. package/lib/helper/ApiDataFactory.js +19 -19
  49. package/lib/helper/Appium.js +47 -51
  50. package/lib/helper/FileSystem.js +35 -15
  51. package/lib/helper/GraphQL.js +1 -1
  52. package/lib/helper/GraphQLDataFactory.js +4 -4
  53. package/lib/helper/JSONResponse.js +72 -45
  54. package/lib/helper/Mochawesome.js +14 -11
  55. package/lib/helper/Playwright.js +832 -434
  56. package/lib/helper/Puppeteer.js +393 -292
  57. package/lib/helper/REST.js +32 -27
  58. package/lib/helper/WebDriver.js +320 -219
  59. package/lib/helper/errors/ConnectionRefused.js +6 -6
  60. package/lib/helper/errors/ElementAssertion.js +11 -16
  61. package/lib/helper/errors/ElementNotFound.js +5 -9
  62. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  63. package/lib/helper/extras/Console.js +11 -11
  64. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  65. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  66. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  67. package/lib/helper/extras/Popup.js +22 -22
  68. package/lib/helper/extras/React.js +29 -30
  69. package/lib/helper/network/actions.js +33 -48
  70. package/lib/helper/network/utils.js +76 -83
  71. package/lib/helper/scripts/blurElement.js +6 -6
  72. package/lib/helper/scripts/focusElement.js +6 -6
  73. package/lib/helper/scripts/highlightElement.js +9 -9
  74. package/lib/helper/scripts/isElementClickable.js +34 -34
  75. package/lib/helper.js +2 -1
  76. package/lib/history.js +23 -20
  77. package/lib/hooks.js +10 -10
  78. package/lib/html.js +90 -100
  79. package/lib/index.js +48 -21
  80. package/lib/listener/config.js +8 -9
  81. package/lib/listener/emptyRun.js +54 -0
  82. package/lib/listener/exit.js +10 -12
  83. package/lib/listener/{retry.js → globalRetry.js} +10 -10
  84. package/lib/listener/globalTimeout.js +166 -0
  85. package/lib/listener/helpers.js +43 -24
  86. package/lib/listener/mocha.js +4 -5
  87. package/lib/listener/result.js +11 -0
  88. package/lib/listener/steps.js +26 -23
  89. package/lib/listener/store.js +20 -0
  90. package/lib/locator.js +213 -192
  91. package/lib/mocha/asyncWrapper.js +264 -0
  92. package/lib/mocha/bdd.js +167 -0
  93. package/lib/mocha/cli.js +341 -0
  94. package/lib/mocha/factory.js +160 -0
  95. package/lib/{interfaces → mocha}/featureConfig.js +33 -13
  96. package/lib/{interfaces → mocha}/gherkin.js +75 -45
  97. package/lib/mocha/hooks.js +121 -0
  98. package/lib/mocha/index.js +21 -0
  99. package/lib/mocha/inject.js +46 -0
  100. package/lib/{interfaces → mocha}/scenarioConfig.js +32 -8
  101. package/lib/mocha/suite.js +89 -0
  102. package/lib/mocha/test.js +178 -0
  103. package/lib/mocha/types.d.ts +42 -0
  104. package/lib/mocha/ui.js +229 -0
  105. package/lib/output.js +86 -64
  106. package/lib/parser.js +44 -44
  107. package/lib/pause.js +160 -139
  108. package/lib/plugin/analyze.js +403 -0
  109. package/lib/plugin/{autoLogin.js → auth.js} +137 -43
  110. package/lib/plugin/autoDelay.js +19 -15
  111. package/lib/plugin/coverage.js +22 -27
  112. package/lib/plugin/customLocator.js +5 -5
  113. package/lib/plugin/customReporter.js +53 -0
  114. package/lib/plugin/heal.js +49 -17
  115. package/lib/plugin/pageInfo.js +140 -0
  116. package/lib/plugin/pauseOnFail.js +4 -3
  117. package/lib/plugin/retryFailedStep.js +60 -19
  118. package/lib/plugin/screenshotOnFail.js +80 -83
  119. package/lib/plugin/stepByStepReport.js +70 -31
  120. package/lib/plugin/stepTimeout.js +7 -13
  121. package/lib/plugin/subtitles.js +10 -9
  122. package/lib/recorder.js +167 -126
  123. package/lib/rerun.js +94 -50
  124. package/lib/result.js +161 -0
  125. package/lib/secret.js +18 -17
  126. package/lib/session.js +95 -89
  127. package/lib/step/base.js +239 -0
  128. package/lib/step/comment.js +10 -0
  129. package/lib/step/config.js +50 -0
  130. package/lib/step/func.js +46 -0
  131. package/lib/step/helper.js +50 -0
  132. package/lib/step/meta.js +99 -0
  133. package/lib/step/record.js +74 -0
  134. package/lib/step/retry.js +11 -0
  135. package/lib/step/section.js +55 -0
  136. package/lib/step.js +18 -332
  137. package/lib/steps.js +54 -0
  138. package/lib/store.js +37 -5
  139. package/lib/template/heal.js +2 -11
  140. package/lib/timeout.js +60 -0
  141. package/lib/transform.js +8 -8
  142. package/lib/translation.js +32 -18
  143. package/lib/utils.js +354 -250
  144. package/lib/workerStorage.js +16 -16
  145. package/lib/workers.js +366 -282
  146. package/package.json +107 -95
  147. package/translations/de-DE.js +5 -4
  148. package/translations/fr-FR.js +5 -4
  149. package/translations/index.js +23 -9
  150. package/translations/it-IT.js +5 -4
  151. package/translations/ja-JP.js +5 -4
  152. package/translations/nl-NL.js +76 -0
  153. package/translations/pl-PL.js +5 -4
  154. package/translations/pt-BR.js +5 -4
  155. package/translations/ru-RU.js +5 -4
  156. package/translations/utils.js +18 -0
  157. package/translations/zh-CN.js +5 -4
  158. package/translations/zh-TW.js +5 -4
  159. package/typings/index.d.ts +177 -186
  160. package/typings/promiseBasedTypes.d.ts +3573 -5941
  161. package/typings/types.d.ts +4042 -6370
  162. package/lib/cli.js +0 -256
  163. package/lib/helper/ExpectHelper.js +0 -391
  164. package/lib/helper/Nightmare.js +0 -1504
  165. package/lib/helper/Protractor.js +0 -1863
  166. package/lib/helper/SoftExpectHelper.js +0 -381
  167. package/lib/helper/TestCafe.js +0 -1414
  168. package/lib/helper/clientscripts/nightmare.js +0 -213
  169. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  170. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  171. package/lib/helper/testcafe/testcafe-utils.js +0 -62
  172. package/lib/interfaces/bdd.js +0 -81
  173. package/lib/listener/artifacts.js +0 -19
  174. package/lib/listener/timeout.js +0 -109
  175. package/lib/mochaFactory.js +0 -113
  176. package/lib/plugin/allure.js +0 -15
  177. package/lib/plugin/commentStep.js +0 -136
  178. package/lib/plugin/debugErrors.js +0 -67
  179. package/lib/plugin/eachElement.js +0 -127
  180. package/lib/plugin/fakerTransform.js +0 -49
  181. package/lib/plugin/retryTo.js +0 -127
  182. package/lib/plugin/selenoid.js +0 -384
  183. package/lib/plugin/standardActingHelpers.js +0 -3
  184. package/lib/plugin/tryTo.js +0 -115
  185. package/lib/plugin/wdio.js +0 -249
  186. package/lib/scenario.js +0 -224
  187. package/lib/ui.js +0 -236
  188. package/lib/within.js +0 -70
package/lib/workers.js CHANGED
@@ -1,54 +1,60 @@
1
- const path = require('path');
2
- const mkdirp = require('mkdirp');
3
- const { Worker } = require('worker_threads');
4
- const { Suite, Test, reporters: { Base } } = require('mocha');
5
- const { EventEmitter } = require('events');
6
- const ms = require('ms');
7
- const Codecept = require('./codecept');
8
- const MochaFactory = require('./mochaFactory');
9
- const Container = require('./container');
10
- const { getTestRoot } = require('./command/utils');
11
- const { isFunction, fileExists } = require('./utils');
12
- const { replaceValueDeep, deepClone } = require('./utils');
13
- const mainConfig = require('./config');
14
- const output = require('./output');
15
- const event = require('./event');
16
- const recorder = require('./recorder');
17
- const runHook = require('./hooks');
18
- const WorkerStorage = require('./workerStorage');
19
- const collection = require('./command/run-multiple/collection');
20
-
21
- const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js');
22
-
23
- const initializeCodecept = (configPath, options = {}) => {
24
- const codecept = new Codecept(mainConfig.load(configPath || '.'), options);
25
- codecept.init(getTestRoot(configPath));
26
- codecept.loadTests();
27
-
28
- return codecept;
29
- };
30
-
31
- const createOutputDir = (configPath) => {
32
- const config = mainConfig.load(configPath || '.');
33
- const testRoot = getTestRoot(configPath);
34
- const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output);
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+ import { dirname } from 'path'
4
+ import { mkdirp } from 'mkdirp'
5
+ import { Worker } from 'worker_threads'
6
+ import { EventEmitter } from 'events'
7
+ import ms from 'ms'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = dirname(__filename)
11
+ import Codecept from './codecept.js'
12
+ import MochaFactory from './mocha/factory.js'
13
+ import Container from './container.js'
14
+ import { getTestRoot } from './command/utils.js'
15
+ import { isFunction, fileExists, replaceValueDeep, deepClone } from './utils.js'
16
+ import mainConfig from './config.js'
17
+ import output from './output.js'
18
+ import event from './event.js'
19
+ import { deserializeTest } from './mocha/test.js'
20
+ import { deserializeSuite } from './mocha/suite.js'
21
+ import recorder from './recorder.js'
22
+ import runHook from './hooks.js'
23
+ import WorkerStorage from './workerStorage.js'
24
+ import { createRuns } from './command/run-multiple/collection.js'
25
+
26
+ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
27
+
28
+ const initializeCodecept = async (configPath, options = {}) => {
29
+ const config = await mainConfig.load(configPath || '.')
30
+ const codecept = new Codecept(config, options)
31
+ await codecept.init(getTestRoot(configPath))
32
+ codecept.loadTests()
33
+
34
+ return codecept
35
+ }
36
+
37
+ const createOutputDir = async configPath => {
38
+ const config = await mainConfig.load(configPath || '.')
39
+ const testRoot = getTestRoot(configPath)
40
+ const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output)
35
41
 
36
42
  if (!fileExists(outputDir)) {
37
- output.print(`creating output directory: ${outputDir}`);
38
- mkdirp.sync(outputDir);
43
+ output.print(`creating output directory: ${outputDir}`)
44
+ mkdirp.sync(outputDir)
39
45
  }
40
- };
46
+ }
41
47
 
42
- const populateGroups = (numberOfWorkers) => {
43
- const groups = [];
48
+ const populateGroups = numberOfWorkers => {
49
+ const groups = []
44
50
  for (let i = 0; i < numberOfWorkers; i++) {
45
- groups[i] = [];
51
+ groups[i] = []
46
52
  }
47
53
 
48
- return groups;
49
- };
54
+ return groups
55
+ }
50
56
 
51
- const createWorker = (workerObject) => {
57
+ const createWorker = workerObject => {
52
58
  const worker = new Worker(pathToWorker, {
53
59
  workerData: {
54
60
  options: simplifyObject(workerObject.options),
@@ -56,170 +62,168 @@ const createWorker = (workerObject) => {
56
62
  testRoot: workerObject.testRoot,
57
63
  workerIndex: workerObject.workerIndex + 1,
58
64
  },
59
- });
60
- worker.on('error', err => output.error(`Worker Error: ${err.stack}`));
65
+ })
66
+ worker.on('error', err => output.error(`Worker Error: ${err.stack}`))
61
67
 
62
- WorkerStorage.addWorker(worker);
63
- return worker;
64
- };
68
+ WorkerStorage.addWorker(worker)
69
+ return worker
70
+ }
65
71
 
66
- const simplifyObject = (object) => {
72
+ const simplifyObject = object => {
67
73
  return Object.keys(object)
68
- .filter((k) => k.indexOf('_') !== 0)
69
- .filter((k) => typeof object[k] !== 'function')
70
- .filter((k) => typeof object[k] !== 'object')
74
+ .filter(k => k.indexOf('_') !== 0)
75
+ .filter(k => typeof object[k] !== 'function')
76
+ .filter(k => typeof object[k] !== 'object')
71
77
  .reduce((obj, key) => {
72
- obj[key] = object[key];
73
- return obj;
74
- }, {});
75
- };
76
-
77
- const repackTest = (test) => {
78
- test = Object.assign(new Test(test.title || '', () => { }), test);
79
- test.parent = Object.assign(new Suite(test.parent.title), test.parent);
80
- return test;
81
- };
78
+ obj[key] = object[key]
79
+ return obj
80
+ }, {})
81
+ }
82
82
 
83
83
  const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns) => {
84
- selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns;
84
+ selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns
85
85
  if (selectedRuns === undefined || !selectedRuns.length || config.multiple === undefined) {
86
86
  return testGroups.map((tests, index) => {
87
- const workerObj = new WorkerObject(index);
88
- workerObj.addConfig(config);
89
- workerObj.addTests(tests);
90
- workerObj.setTestRoot(testRoot);
91
- workerObj.addOptions(options);
92
- return workerObj;
93
- });
87
+ const workerObj = new WorkerObject(index)
88
+ workerObj.addConfig(config)
89
+ workerObj.addTests(tests)
90
+ workerObj.setTestRoot(testRoot)
91
+ workerObj.addOptions(options)
92
+ return workerObj
93
+ })
94
94
  }
95
- const workersToExecute = [];
95
+ const workersToExecute = []
96
96
 
97
- const currentOutputFolder = config.output;
98
- let currentMochawesomeReportDir;
99
- let currentMochaJunitReporterFile;
97
+ const currentOutputFolder = config.output
98
+ let currentMochawesomeReportDir
99
+ let currentMochaJunitReporterFile
100
100
 
101
101
  if (config.mocha && config.mocha.reporterOptions) {
102
- currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir;
103
- currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile;
102
+ currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir
103
+ currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile
104
104
  }
105
105
 
106
- collection.createRuns(selectedRuns, config).forEach((worker) => {
107
- const separator = path.sep;
108
- const _config = { ...config };
109
- let workerName = worker.name.replace(':', '_');
110
- _config.output = `${currentOutputFolder}${separator}${workerName}`;
106
+ createRuns(selectedRuns, config).forEach(worker => {
107
+ const separator = path.sep
108
+ const _config = { ...config }
109
+ let workerName = worker.name.replace(':', '_')
110
+ _config.output = `${currentOutputFolder}${separator}${workerName}`
111
111
  if (config.mocha && config.mocha.reporterOptions) {
112
- _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`;
113
-
114
- const _tempArray = currentMochaJunitReporterFile.split(separator);
115
- _tempArray.splice(_tempArray.findIndex(item => item.includes('.xml')), 0, workerName);
116
- _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator);
112
+ _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`
113
+
114
+ const _tempArray = currentMochaJunitReporterFile.split(separator)
115
+ _tempArray.splice(
116
+ _tempArray.findIndex(item => item.includes('.xml')),
117
+ 0,
118
+ workerName,
119
+ )
120
+ _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator)
117
121
  }
118
- workerName = worker.getOriginalName() || worker.getName();
119
- const workerConfig = worker.getConfig();
120
- workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config));
121
- });
122
- const workers = [];
123
- let index = 0;
124
- testGroups.forEach((tests) => {
125
- const testWorkerArray = [];
126
- workersToExecute.forEach((finalConfig) => {
127
- const workerObj = new WorkerObject(index++);
128
- workerObj.addConfig(finalConfig);
129
- workerObj.addTests(tests);
130
- workerObj.setTestRoot(testRoot);
131
- workerObj.addOptions(options);
132
- testWorkerArray.push(workerObj);
133
- });
134
- workers.push(...testWorkerArray);
135
- });
136
- return workers;
137
- };
138
-
139
- const indexOfSmallestElement = (groups) => {
140
- let i = 0;
122
+ workerName = worker.getOriginalName() || worker.getName()
123
+ const workerConfig = worker.getConfig()
124
+ workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config))
125
+ })
126
+ const workers = []
127
+ let index = 0
128
+ testGroups.forEach(tests => {
129
+ const testWorkerArray = []
130
+ workersToExecute.forEach(finalConfig => {
131
+ const workerObj = new WorkerObject(index++)
132
+ workerObj.addConfig(finalConfig)
133
+ workerObj.addTests(tests)
134
+ workerObj.setTestRoot(testRoot)
135
+ workerObj.addOptions(options)
136
+ testWorkerArray.push(workerObj)
137
+ })
138
+ workers.push(...testWorkerArray)
139
+ })
140
+ return workers
141
+ }
142
+
143
+ const indexOfSmallestElement = groups => {
144
+ let i = 0
141
145
  for (let j = 1; j < groups.length; j++) {
142
146
  if (groups[j - 1].length > groups[j].length) {
143
- i = j;
147
+ i = j
144
148
  }
145
149
  }
146
- return i;
147
- };
150
+ return i
151
+ }
148
152
 
149
- const convertToMochaTests = (testGroup) => {
150
- const group = [];
153
+ const convertToMochaTests = testGroup => {
154
+ const group = []
151
155
  if (testGroup instanceof Array) {
152
- const mocha = MochaFactory.create({}, {});
153
- mocha.files = testGroup;
154
- mocha.loadFiles();
155
- mocha.suite.eachTest((test) => {
156
- group.push(test.uid);
157
- });
158
- mocha.unloadFiles();
156
+ const mocha = MochaFactory.create({}, {})
157
+ mocha.files = testGroup
158
+ mocha.loadFiles()
159
+ mocha.suite.eachTest(test => {
160
+ group.push(test.uid)
161
+ })
162
+ mocha.unloadFiles()
159
163
  }
160
164
 
161
- return group;
162
- };
165
+ return group
166
+ }
163
167
 
164
168
  const getOverridenConfig = (workerName, workerConfig, config) => {
165
169
  // clone config
166
- const overriddenConfig = deepClone(config);
170
+ const overriddenConfig = deepClone(config)
167
171
 
168
172
  // get configuration
169
- const browserConfig = workerConfig.browser;
173
+ const browserConfig = workerConfig.browser
170
174
 
171
175
  for (const key in browserConfig) {
172
- overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]);
176
+ overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key])
173
177
  }
174
178
 
175
179
  // override tests configuration
176
180
  if (overriddenConfig.tests) {
177
- overriddenConfig.tests = workerConfig.tests;
181
+ overriddenConfig.tests = workerConfig.tests
178
182
  }
179
183
 
180
184
  if (overriddenConfig.gherkin && workerConfig.gherkin && workerConfig.gherkin.features) {
181
- overriddenConfig.gherkin.features = workerConfig.gherkin.features;
185
+ overriddenConfig.gherkin.features = workerConfig.gherkin.features
182
186
  }
183
- return overriddenConfig;
184
- };
187
+ return overriddenConfig
188
+ }
185
189
 
186
190
  class WorkerObject {
187
191
  /**
188
192
  * @param {Number} workerIndex - Unique ID for worker
189
193
  */
190
194
  constructor(workerIndex) {
191
- this.workerIndex = workerIndex;
192
- this.options = {};
193
- this.tests = [];
194
- this.testRoot = getTestRoot();
195
+ this.workerIndex = workerIndex
196
+ this.options = {}
197
+ this.tests = []
198
+ this.testRoot = getTestRoot()
195
199
  }
196
200
 
197
201
  addConfig(config) {
198
- const oldConfig = JSON.parse(this.options.override || '{}');
202
+ const oldConfig = JSON.parse(this.options.override || '{}')
199
203
  const newConfig = {
200
204
  ...oldConfig,
201
205
  ...config,
202
- };
203
- this.options.override = JSON.stringify(newConfig);
206
+ }
207
+ this.options.override = JSON.stringify(newConfig)
204
208
  }
205
209
 
206
210
  addTestFiles(testGroup) {
207
- this.addTests(convertToMochaTests(testGroup));
211
+ this.addTests(convertToMochaTests(testGroup))
208
212
  }
209
213
 
210
214
  addTests(tests) {
211
- this.tests = this.tests.concat(tests);
215
+ this.tests = this.tests.concat(tests)
212
216
  }
213
217
 
214
218
  setTestRoot(path) {
215
- this.testRoot = getTestRoot(path);
219
+ this.testRoot = getTestRoot(path)
216
220
  }
217
221
 
218
222
  addOptions(opts) {
219
223
  this.options = {
220
224
  ...this.options,
221
225
  ...opts,
222
- };
226
+ }
223
227
  }
224
228
  }
225
229
 
@@ -229,30 +233,47 @@ class Workers extends EventEmitter {
229
233
  * @param {Object} config
230
234
  */
231
235
  constructor(numberOfWorkers, config = { by: 'test' }) {
232
- super();
233
- this.setMaxListeners(50);
234
- this.codecept = initializeCodecept(config.testConfig, config.options);
235
- this.failuresLog = [];
236
- this.errors = [];
237
- this.numberOfWorkers = 0;
238
- this.closedWorkers = 0;
239
- this.workers = [];
240
- this.stats = {
241
- passes: 0,
242
- failures: 0,
243
- tests: 0,
244
- pending: 0,
245
- };
246
- this.testGroups = [];
247
-
248
- createOutputDir(config.testConfig);
249
- if (numberOfWorkers) this._initWorkers(numberOfWorkers, config);
236
+ super()
237
+ this.setMaxListeners(50)
238
+ this.codeceptPromise = initializeCodecept(config.testConfig, config.options)
239
+ this.codecept = null
240
+ this.errors = []
241
+ this.numberOfWorkers = 0
242
+ this.closedWorkers = 0
243
+ this.workers = []
244
+ this.testGroups = []
245
+ this.config = config
246
+ this.numberOfWorkersRequested = numberOfWorkers
247
+ // Track emitted pass events to avoid double-counting duplicates from retries/race conditions
248
+ this._passedUids = new Set()
249
+
250
+ createOutputDir(config.testConfig)
251
+ // Defer worker initialization until codecept is ready
252
+ }
253
+
254
+ async _ensureInitialized() {
255
+ if (!this.codecept) {
256
+ this.codecept = await this.codeceptPromise
257
+ // Initialize workers in these cases:
258
+ // 1. Positive number requested AND no manual workers pre-spawned
259
+ // 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned
260
+ const shouldAutoInit = this.workers.length === 0 && (
261
+ (Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) ||
262
+ (this.numberOfWorkersRequested < 0 && isFunction(this.config.by))
263
+ )
264
+
265
+ if (shouldAutoInit) {
266
+ this._initWorkers(this.numberOfWorkersRequested, this.config)
267
+ }
268
+ }
250
269
  }
251
270
 
252
271
  _initWorkers(numberOfWorkers, config) {
253
- this.splitTestsByGroups(numberOfWorkers, config);
254
- this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns);
255
- this.numberOfWorkers = this.workers.length;
272
+ this.splitTestsByGroups(numberOfWorkers, config)
273
+ // For function-based grouping, use the actual number of test groups created
274
+ const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
275
+ this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
276
+ this.numberOfWorkers = this.workers.length
256
277
  }
257
278
 
258
279
  /**
@@ -269,16 +290,16 @@ class Workers extends EventEmitter {
269
290
  */
270
291
  splitTestsByGroups(numberOfWorkers, config) {
271
292
  if (isFunction(config.by)) {
272
- const createTests = config.by;
273
- const testGroups = createTests(numberOfWorkers);
293
+ const createTests = config.by
294
+ const testGroups = createTests(numberOfWorkers)
274
295
  if (!(testGroups instanceof Array)) {
275
- throw new Error('Test group should be an array');
296
+ throw new Error('Test group should be an array')
276
297
  }
277
298
  for (const testGroup of testGroups) {
278
- this.testGroups.push(convertToMochaTests(testGroup));
299
+ this.testGroups.push(convertToMochaTests(testGroup))
279
300
  }
280
301
  } else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) {
281
- this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers);
302
+ this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
282
303
  }
283
304
  }
284
305
 
@@ -288,53 +309,57 @@ class Workers extends EventEmitter {
288
309
  * @returns {WorkerObject}
289
310
  */
290
311
  spawn() {
291
- const worker = new WorkerObject(this.numberOfWorkers);
292
- this.workers.push(worker);
293
- this.numberOfWorkers += 1;
294
- return worker;
312
+ const worker = new WorkerObject(this.numberOfWorkers)
313
+ this.workers.push(worker)
314
+ this.numberOfWorkers += 1
315
+ return worker
295
316
  }
296
317
 
297
318
  /**
298
319
  * @param {Number} numberOfWorkers
299
320
  */
300
321
  createGroupsOfTests(numberOfWorkers) {
301
- const files = this.codecept.testFiles;
302
- const mocha = Container.mocha();
303
- mocha.files = files;
304
- mocha.loadFiles();
305
-
306
- const groups = populateGroups(numberOfWorkers);
307
- let groupCounter = 0;
308
-
309
- mocha.suite.eachTest((test) => {
310
- const i = groupCounter % groups.length;
322
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
323
+ if (!this.codecept) return populateGroups(numberOfWorkers)
324
+ const files = this.codecept.testFiles
325
+ const mocha = Container.mocha()
326
+ mocha.files = files
327
+ mocha.loadFiles()
328
+
329
+ const groups = populateGroups(numberOfWorkers)
330
+ let groupCounter = 0
331
+
332
+ mocha.suite.eachTest(test => {
333
+ const i = groupCounter % groups.length
311
334
  if (test) {
312
- groups[i].push(test.uid);
313
- groupCounter++;
335
+ groups[i].push(test.uid)
336
+ groupCounter++
314
337
  }
315
- });
316
- return groups;
338
+ })
339
+ return groups
317
340
  }
318
341
 
319
342
  /**
320
343
  * @param {Number} numberOfWorkers
321
344
  */
322
345
  createGroupsOfSuites(numberOfWorkers) {
323
- const files = this.codecept.testFiles;
324
- const groups = populateGroups(numberOfWorkers);
325
-
326
- const mocha = Container.mocha();
327
- mocha.files = files;
328
- mocha.loadFiles();
329
- mocha.suite.suites.forEach((suite) => {
330
- const i = indexOfSmallestElement(groups);
331
- suite.tests.forEach((test) => {
346
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
347
+ if (!this.codecept) return populateGroups(numberOfWorkers)
348
+ const files = this.codecept.testFiles
349
+ const groups = populateGroups(numberOfWorkers)
350
+
351
+ const mocha = Container.mocha()
352
+ mocha.files = files
353
+ mocha.loadFiles()
354
+ mocha.suite.suites.forEach(suite => {
355
+ const i = indexOfSmallestElement(groups)
356
+ suite.tests.forEach(test => {
332
357
  if (test) {
333
- groups[i].push(test.uid);
358
+ groups[i].push(test.uid)
334
359
  }
335
- });
336
- });
337
- return groups;
360
+ })
361
+ })
362
+ return groups
338
363
  }
339
364
 
340
365
  /**
@@ -342,160 +367,219 @@ class Workers extends EventEmitter {
342
367
  */
343
368
  overrideConfig(config) {
344
369
  for (const worker of this.workers) {
345
- worker.addConfig(config);
370
+ worker.addConfig(config)
346
371
  }
347
372
  }
348
373
 
349
374
  async bootstrapAll() {
350
- return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll');
375
+ await this._ensureInitialized()
376
+ return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll')
351
377
  }
352
378
 
353
379
  async teardownAll() {
354
- return runHook(this.codecept.config.teardownAll, 'teardownAll');
380
+ await this._ensureInitialized()
381
+ return runHook(this.codecept.config.teardownAll, 'teardownAll')
355
382
  }
356
383
 
357
- run() {
358
- this.stats.start = new Date();
359
- this.stats.failedHooks = 0
360
- recorder.startUnlessRunning();
361
- event.dispatcher.emit(event.workers.before);
362
- process.env.RUNS_WITH_WORKERS = 'true';
384
+ async run() {
385
+ await this._ensureInitialized()
386
+ recorder.startUnlessRunning()
387
+ event.dispatcher.emit(event.workers.before)
388
+ process.env.RUNS_WITH_WORKERS = 'true'
363
389
  recorder.add('starting workers', () => {
364
390
  for (const worker of this.workers) {
365
- const workerThread = createWorker(worker);
366
- this._listenWorkerEvents(workerThread);
391
+ const workerThread = createWorker(worker)
392
+ this._listenWorkerEvents(workerThread)
367
393
  }
368
- });
394
+ })
369
395
  return new Promise(resolve => {
370
- this.on('end', resolve);
371
- });
396
+ this.on('end', resolve)
397
+ })
372
398
  }
373
399
 
374
400
  /**
375
401
  * @returns {Array<WorkerObject>}
376
402
  */
377
403
  getWorkers() {
378
- return this.workers;
404
+ return this.workers
379
405
  }
380
406
 
381
407
  /**
382
408
  * @returns {Boolean}
383
409
  */
384
410
  isFailed() {
385
- return (this.stats.failures || this.errors.length) > 0;
411
+ return (Container.result().failures.length || this.errors.length) > 0
386
412
  }
387
413
 
388
414
  _listenWorkerEvents(worker) {
389
- worker.on('message', (message) => {
390
- output.process(message.workerIndex);
415
+ worker.on('message', message => {
416
+ output.process(message.workerIndex)
391
417
 
392
418
  // deal with events that are not test cycle related
393
419
  if (!message.event) {
394
- return this.emit('message', message);
420
+ return this.emit('message', message)
395
421
  }
396
422
 
397
423
  switch (message.event) {
398
- case event.all.failures:
399
- this.failuresLog = this.failuresLog.concat(message.data.failuresLog);
400
- this._appendStats(message.data.stats);
401
- break;
424
+ case event.all.result:
425
+ // we ensure consistency of result by adding tests in the very end
426
+ Container.result().addFailures(message.data.failures)
427
+ Container.result().addStats(message.data.stats)
428
+ message.data.tests.forEach(test => {
429
+ Container.result().addTest(deserializeTest(test))
430
+ })
431
+ break
402
432
  case event.suite.before:
403
- this.emit(event.suite.before, repackTest(message.data));
404
- break;
405
- case event.hook.failed:
406
- this.emit(event.hook.failed, repackTest(message.data));
407
- this.errors.push(message.data.err);
408
- break;
433
+ this.emit(event.suite.before, deserializeSuite(message.data))
434
+ break
409
435
  case event.test.before:
410
- this.emit(event.test.before, repackTest(message.data));
411
- break;
436
+ this.emit(event.test.before, deserializeTest(message.data))
437
+ break
412
438
  case event.test.started:
413
- this.emit(event.test.started, repackTest(message.data));
414
- break;
439
+ this.emit(event.test.started, deserializeTest(message.data))
440
+ break
415
441
  case event.test.failed:
416
- this.emit(event.test.failed, repackTest(message.data));
417
- break;
442
+ // Skip individual failed events - we'll emit based on finished state
443
+ break
418
444
  case event.test.passed:
419
- this.emit(event.test.passed, repackTest(message.data));
420
- break;
445
+ // Skip individual passed events - we'll emit based on finished state
446
+ break
421
447
  case event.test.skipped:
422
- this.emit(event.test.skipped, repackTest(message.data));
423
- break;
448
+ this.emit(event.test.skipped, deserializeTest(message.data))
449
+ break
424
450
  case event.test.finished:
425
- this.emit(event.test.finished, repackTest(message.data));
426
- break;
451
+ // Handle different types of test completion properly
452
+ {
453
+ const data = message.data
454
+ const uid = data?.uid
455
+ const isFailed = !!data?.err || data?.state === 'failed'
456
+
457
+ if (uid) {
458
+ // Track states for each test UID
459
+ if (!this._testStates) this._testStates = new Map()
460
+
461
+ if (!this._testStates.has(uid)) {
462
+ this._testStates.set(uid, { states: [], lastData: data })
463
+ }
464
+
465
+ const testState = this._testStates.get(uid)
466
+ testState.states.push({ isFailed, data })
467
+ testState.lastData = data
468
+ } else {
469
+ // For tests without UID, emit immediately
470
+ if (isFailed) {
471
+ this.emit(event.test.failed, deserializeTest(data))
472
+ } else {
473
+ this.emit(event.test.passed, deserializeTest(data))
474
+ }
475
+ }
476
+
477
+ this.emit(event.test.finished, deserializeTest(data))
478
+ }
479
+ break
427
480
  case event.test.after:
428
- this.emit(event.test.after, repackTest(message.data));
429
- break;
481
+ this.emit(event.test.after, deserializeTest(message.data))
482
+ break
430
483
  case event.step.finished:
431
- this.emit(event.step.finished, message.data);
432
- break;
484
+ this.emit(event.step.finished, message.data)
485
+ break
433
486
  case event.step.started:
434
- this.emit(event.step.started, message.data);
435
- break;
487
+ this.emit(event.step.started, message.data)
488
+ break
436
489
  case event.step.passed:
437
- this.emit(event.step.passed, message.data);
438
- break;
490
+ this.emit(event.step.passed, message.data)
491
+ break
439
492
  case event.step.failed:
440
- this.emit(event.step.failed, message.data);
441
- break;
493
+ this.emit(event.step.failed, message.data, message.data.error)
494
+ break
495
+ case event.hook.failed:
496
+ // Count hook failures as test failures for event counting
497
+ this.emit(event.test.failed, { title: `Hook failure: ${message.data.hookName || 'unknown'}`, err: message.data.error })
498
+ this.emit(event.hook.failed, message.data)
499
+ break
442
500
  }
443
- });
501
+ })
444
502
 
445
- worker.on('error', (err) => {
446
- this.errors.push(err);
447
- });
503
+ worker.on('error', err => {
504
+ this.errors.push(err)
505
+ })
448
506
 
449
507
  worker.on('exit', () => {
450
- this.closedWorkers += 1;
508
+ this.closedWorkers += 1
451
509
  if (this.closedWorkers === this.numberOfWorkers) {
452
- this._finishRun();
510
+ this._finishRun()
453
511
  }
454
- });
512
+ })
455
513
  }
456
514
 
457
515
  _finishRun() {
458
- event.dispatcher.emit(event.workers.after);
459
- if (this.isFailed()) {
460
- process.exitCode = 1;
516
+ event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
517
+ if (Container.result().hasFailed) {
518
+ process.exitCode = 1
461
519
  } else {
462
- process.exitCode = 0;
520
+ process.exitCode = 0
463
521
  }
464
- // removed this.finishedTests because in all /lib only first argument (!this.isFailed()) is used)
465
- this.emit(event.all.result, !this.isFailed());
466
- this.emit('end'); // internal event
467
- }
468
522
 
469
- _appendStats(newStats) {
470
- this.stats.passes += newStats.passes;
471
- this.stats.failures += newStats.failures;
472
- this.stats.tests += newStats.tests;
473
- this.stats.pending += newStats.pending;
474
- this.stats.failedHooks += newStats.failedHooks;
523
+ // Emit states for all tracked tests before emitting results
524
+ if (this._testStates) {
525
+ for (const [uid, { states, lastData }] of this._testStates) {
526
+ // For tests with retries configured, emit all failures + final success
527
+ // For tests without retries, emit only final state
528
+ const lastState = states[states.length - 1]
529
+
530
+ // Check if this test had retries by looking for failure followed by success
531
+ const hasRetryPattern = states.length > 1 &&
532
+ states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
533
+
534
+ if (hasRetryPattern) {
535
+ // Emit all intermediate failures and final success for retries
536
+ for (const state of states) {
537
+ if (state.isFailed) {
538
+ this.emit(event.test.failed, deserializeTest(state.data))
539
+ } else {
540
+ this.emit(event.test.passed, deserializeTest(state.data))
541
+ }
542
+ }
543
+ } else {
544
+ // For non-retries (like step failures), emit only the final state
545
+ if (lastState.isFailed) {
546
+ this.emit(event.test.failed, deserializeTest(lastState.data))
547
+ } else {
548
+ this.emit(event.test.passed, deserializeTest(lastState.data))
549
+ }
550
+ }
551
+ }
552
+ this._testStates.clear()
553
+ }
554
+
555
+ this.emit(event.all.result, Container.result())
556
+ event.dispatcher.emit(event.workers.result, Container.result())
557
+ this.emit('end') // internal event
475
558
  }
476
559
 
477
560
  printResults() {
478
- this.stats.end = new Date();
479
- this.stats.duration = this.stats.end - this.stats.start;
561
+ const result = Container.result()
562
+ result.finish()
480
563
 
481
564
  // Reset process for logs in main thread
482
- output.process(null);
483
- output.print();
565
+ output.process(null)
566
+ output.print()
484
567
 
485
- this.failuresLog = this.failuresLog
568
+ this.failuresLog = result.failures
486
569
  .filter(log => log.length && typeof log[1] === 'number')
487
570
  // mocha/lib/reporters/base.js
488
- .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack]);
571
+ .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack])
489
572
 
490
573
  if (this.failuresLog.length) {
491
- output.print();
492
- output.print('-- FAILURES:');
493
- this.failuresLog.forEach(log => output.print(...log));
574
+ output.print()
575
+ output.print('-- FAILURES:')
576
+ this.failuresLog.forEach(log => output.print(...log))
494
577
  }
495
578
 
496
- output.result(this.stats.passes, this.stats.failures, this.stats.pending, ms(this.stats.duration), this.stats.failedHooks);
497
- process.env.RUNS_WITH_WORKERS = 'false';
579
+ output.result(result.stats?.passes || 0, result.stats?.failures || 0, result.stats?.pending || 0, ms(result.duration), result.stats?.failedHooks || 0)
580
+
581
+ process.env.RUNS_WITH_WORKERS = 'false'
498
582
  }
499
583
  }
500
584
 
501
- module.exports = Workers;
585
+ export default Workers