codeceptjs 4.0.0-beta.4 → 4.0.0-beta.5

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 (150) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +139 -87
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/generate.js +10 -14
  17. package/lib/command/gherkin/snippets.js +75 -73
  18. package/lib/command/gherkin/steps.js +1 -1
  19. package/lib/command/info.js +42 -8
  20. package/lib/command/init.js +13 -12
  21. package/lib/command/interactive.js +10 -2
  22. package/lib/command/list.js +1 -1
  23. package/lib/command/run-multiple/chunk.js +48 -45
  24. package/lib/command/run-multiple.js +12 -35
  25. package/lib/command/run-workers.js +21 -58
  26. package/lib/command/utils.js +5 -6
  27. package/lib/command/workers/runTests.js +262 -220
  28. package/lib/container.js +386 -238
  29. package/lib/data/context.js +10 -13
  30. package/lib/data/dataScenarioConfig.js +8 -8
  31. package/lib/data/dataTableArgument.js +6 -6
  32. package/lib/data/table.js +5 -11
  33. package/lib/effects.js +223 -0
  34. package/lib/element/WebElement.js +327 -0
  35. package/lib/els.js +158 -0
  36. package/lib/event.js +21 -17
  37. package/lib/heal.js +88 -80
  38. package/lib/helper/AI.js +2 -1
  39. package/lib/helper/ApiDataFactory.js +3 -6
  40. package/lib/helper/Appium.js +47 -51
  41. package/lib/helper/FileSystem.js +3 -3
  42. package/lib/helper/GraphQLDataFactory.js +3 -3
  43. package/lib/helper/JSONResponse.js +75 -37
  44. package/lib/helper/Mochawesome.js +31 -9
  45. package/lib/helper/Nightmare.js +35 -53
  46. package/lib/helper/Playwright.js +262 -267
  47. package/lib/helper/Protractor.js +54 -77
  48. package/lib/helper/Puppeteer.js +246 -260
  49. package/lib/helper/REST.js +5 -17
  50. package/lib/helper/TestCafe.js +21 -44
  51. package/lib/helper/WebDriver.js +151 -170
  52. package/lib/helper/extras/Popup.js +22 -22
  53. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  54. package/lib/listener/emptyRun.js +55 -0
  55. package/lib/listener/exit.js +7 -10
  56. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  57. package/lib/listener/globalTimeout.js +165 -0
  58. package/lib/listener/helpers.js +15 -15
  59. package/lib/listener/mocha.js +1 -1
  60. package/lib/listener/result.js +12 -0
  61. package/lib/listener/retryEnhancer.js +85 -0
  62. package/lib/listener/steps.js +32 -18
  63. package/lib/listener/store.js +20 -0
  64. package/lib/mocha/asyncWrapper.js +231 -0
  65. package/lib/{interfaces → mocha}/bdd.js +3 -3
  66. package/lib/mocha/cli.js +308 -0
  67. package/lib/mocha/factory.js +104 -0
  68. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  69. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  70. package/lib/mocha/hooks.js +112 -0
  71. package/lib/mocha/index.js +12 -0
  72. package/lib/mocha/inject.js +29 -0
  73. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  74. package/lib/mocha/suite.js +82 -0
  75. package/lib/mocha/test.js +181 -0
  76. package/lib/mocha/types.d.ts +42 -0
  77. package/lib/mocha/ui.js +232 -0
  78. package/lib/output.js +82 -62
  79. package/lib/pause.js +160 -138
  80. package/lib/plugin/analyze.js +396 -0
  81. package/lib/plugin/auth.js +435 -0
  82. package/lib/plugin/autoDelay.js +8 -8
  83. package/lib/plugin/autoLogin.js +3 -338
  84. package/lib/plugin/commentStep.js +6 -1
  85. package/lib/plugin/coverage.js +10 -19
  86. package/lib/plugin/customLocator.js +3 -3
  87. package/lib/plugin/customReporter.js +52 -0
  88. package/lib/plugin/eachElement.js +1 -1
  89. package/lib/plugin/fakerTransform.js +1 -1
  90. package/lib/plugin/heal.js +36 -9
  91. package/lib/plugin/htmlReporter.js +1947 -0
  92. package/lib/plugin/pageInfo.js +140 -0
  93. package/lib/plugin/retryFailedStep.js +17 -18
  94. package/lib/plugin/retryTo.js +2 -113
  95. package/lib/plugin/screenshotOnFail.js +17 -58
  96. package/lib/plugin/selenoid.js +15 -35
  97. package/lib/plugin/standardActingHelpers.js +4 -1
  98. package/lib/plugin/stepByStepReport.js +56 -17
  99. package/lib/plugin/stepTimeout.js +5 -12
  100. package/lib/plugin/subtitles.js +4 -4
  101. package/lib/plugin/tryTo.js +3 -102
  102. package/lib/plugin/wdio.js +8 -10
  103. package/lib/recorder.js +155 -124
  104. package/lib/rerun.js +43 -42
  105. package/lib/result.js +161 -0
  106. package/lib/secret.js +1 -1
  107. package/lib/step/base.js +239 -0
  108. package/lib/step/comment.js +10 -0
  109. package/lib/step/config.js +50 -0
  110. package/lib/step/func.js +46 -0
  111. package/lib/step/helper.js +50 -0
  112. package/lib/step/meta.js +99 -0
  113. package/lib/step/record.js +74 -0
  114. package/lib/step/retry.js +11 -0
  115. package/lib/step/section.js +55 -0
  116. package/lib/step.js +21 -332
  117. package/lib/steps.js +50 -0
  118. package/lib/store.js +37 -5
  119. package/lib/template/heal.js +2 -11
  120. package/lib/test-server.js +323 -0
  121. package/lib/timeout.js +66 -0
  122. package/lib/utils.js +351 -218
  123. package/lib/within.js +75 -55
  124. package/lib/workerStorage.js +2 -1
  125. package/lib/workers.js +386 -276
  126. package/package.json +76 -70
  127. package/translations/de-DE.js +4 -3
  128. package/translations/fr-FR.js +4 -3
  129. package/translations/index.js +1 -0
  130. package/translations/it-IT.js +4 -3
  131. package/translations/ja-JP.js +4 -3
  132. package/translations/nl-NL.js +76 -0
  133. package/translations/pl-PL.js +4 -3
  134. package/translations/pt-BR.js +4 -3
  135. package/translations/ru-RU.js +4 -3
  136. package/translations/utils.js +9 -0
  137. package/translations/zh-CN.js +4 -3
  138. package/translations/zh-TW.js +4 -3
  139. package/typings/index.d.ts +188 -186
  140. package/typings/promiseBasedTypes.d.ts +18 -705
  141. package/typings/types.d.ts +301 -804
  142. package/lib/cli.js +0 -256
  143. package/lib/helper/ExpectHelper.js +0 -391
  144. package/lib/helper/SoftExpectHelper.js +0 -381
  145. package/lib/listener/artifacts.js +0 -19
  146. package/lib/listener/timeout.js +0 -109
  147. package/lib/mochaFactory.js +0 -113
  148. package/lib/plugin/debugErrors.js +0 -67
  149. package/lib/scenario.js +0 -224
  150. package/lib/ui.js +0 -236
package/lib/workers.js CHANGED
@@ -1,225 +1,225 @@
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');
1
+ const path = require('path')
2
+ const mkdirp = require('mkdirp')
3
+ const { Worker } = require('worker_threads')
4
+ const { EventEmitter } = require('events')
5
+ const ms = require('ms')
6
+ const Codecept = require('./codecept')
7
+ const MochaFactory = require('./mocha/factory')
8
+ const Container = require('./container')
9
+ const { getTestRoot } = require('./command/utils')
10
+ const { isFunction, fileExists } = require('./utils')
11
+ const { replaceValueDeep, deepClone } = require('./utils')
12
+ const mainConfig = require('./config')
13
+ const output = require('./output')
14
+ const event = require('./event')
15
+ const { deserializeTest } = require('./mocha/test')
16
+ const { deserializeSuite } = require('./mocha/suite')
17
+ const recorder = require('./recorder')
18
+ const runHook = require('./hooks')
19
+ const WorkerStorage = require('./workerStorage')
20
+ const collection = require('./command/run-multiple/collection')
21
+
22
+ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
22
23
 
23
24
  const initializeCodecept = (configPath, options = {}) => {
24
- const codecept = new Codecept(mainConfig.load(configPath || '.'), options);
25
- codecept.init(getTestRoot(configPath));
26
- codecept.loadTests();
25
+ const codecept = new Codecept(mainConfig.load(configPath || '.'), options)
26
+ codecept.init(getTestRoot(configPath))
27
+ codecept.loadTests()
27
28
 
28
- return codecept;
29
- };
29
+ return codecept
30
+ }
30
31
 
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);
32
+ const createOutputDir = configPath => {
33
+ const config = mainConfig.load(configPath || '.')
34
+ const testRoot = getTestRoot(configPath)
35
+ const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output)
35
36
 
36
37
  if (!fileExists(outputDir)) {
37
- output.print(`creating output directory: ${outputDir}`);
38
- mkdirp.sync(outputDir);
38
+ output.print(`creating output directory: ${outputDir}`)
39
+ mkdirp.sync(outputDir)
39
40
  }
40
- };
41
+ }
41
42
 
42
- const populateGroups = (numberOfWorkers) => {
43
- const groups = [];
43
+ const populateGroups = numberOfWorkers => {
44
+ const groups = []
44
45
  for (let i = 0; i < numberOfWorkers; i++) {
45
- groups[i] = [];
46
+ groups[i] = []
46
47
  }
47
48
 
48
- return groups;
49
- };
49
+ return groups
50
+ }
50
51
 
51
- const createWorker = (workerObject) => {
52
+ const createWorker = (workerObject, isPoolMode = false) => {
52
53
  const worker = new Worker(pathToWorker, {
53
54
  workerData: {
54
55
  options: simplifyObject(workerObject.options),
55
56
  tests: workerObject.tests,
56
57
  testRoot: workerObject.testRoot,
57
58
  workerIndex: workerObject.workerIndex + 1,
59
+ poolMode: isPoolMode,
58
60
  },
59
- });
60
- worker.on('error', err => output.error(`Worker Error: ${err.stack}`));
61
+ })
62
+ worker.on('error', err => output.error(`Worker Error: ${err.stack}`))
61
63
 
62
- WorkerStorage.addWorker(worker);
63
- return worker;
64
- };
64
+ WorkerStorage.addWorker(worker)
65
+ return worker
66
+ }
65
67
 
66
- const simplifyObject = (object) => {
68
+ const simplifyObject = object => {
67
69
  return Object.keys(object)
68
- .filter((k) => k.indexOf('_') !== 0)
69
- .filter((k) => typeof object[k] !== 'function')
70
- .filter((k) => typeof object[k] !== 'object')
70
+ .filter(k => k.indexOf('_') !== 0)
71
+ .filter(k => typeof object[k] !== 'function')
72
+ .filter(k => typeof object[k] !== 'object')
71
73
  .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
- };
74
+ obj[key] = object[key]
75
+ return obj
76
+ }, {})
77
+ }
82
78
 
83
79
  const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns) => {
84
- selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns;
80
+ selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns
85
81
  if (selectedRuns === undefined || !selectedRuns.length || config.multiple === undefined) {
86
82
  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
- });
83
+ const workerObj = new WorkerObject(index)
84
+ workerObj.addConfig(config)
85
+ workerObj.addTests(tests)
86
+ workerObj.setTestRoot(testRoot)
87
+ workerObj.addOptions(options)
88
+ return workerObj
89
+ })
94
90
  }
95
- const workersToExecute = [];
91
+ const workersToExecute = []
96
92
 
97
- const currentOutputFolder = config.output;
98
- let currentMochawesomeReportDir;
99
- let currentMochaJunitReporterFile;
93
+ const currentOutputFolder = config.output
94
+ let currentMochawesomeReportDir
95
+ let currentMochaJunitReporterFile
100
96
 
101
97
  if (config.mocha && config.mocha.reporterOptions) {
102
- currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir;
103
- currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile;
98
+ currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir
99
+ currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile
104
100
  }
105
101
 
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}`;
102
+ collection.createRuns(selectedRuns, config).forEach(worker => {
103
+ const separator = path.sep
104
+ const _config = { ...config }
105
+ let workerName = worker.name.replace(':', '_')
106
+ _config.output = `${currentOutputFolder}${separator}${workerName}`
111
107
  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);
108
+ _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`
109
+
110
+ const _tempArray = currentMochaJunitReporterFile.split(separator)
111
+ _tempArray.splice(
112
+ _tempArray.findIndex(item => item.includes('.xml')),
113
+ 0,
114
+ workerName,
115
+ )
116
+ _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator)
117
117
  }
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;
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
141
141
  for (let j = 1; j < groups.length; j++) {
142
142
  if (groups[j - 1].length > groups[j].length) {
143
- i = j;
143
+ i = j
144
144
  }
145
145
  }
146
- return i;
147
- };
146
+ return i
147
+ }
148
148
 
149
- const convertToMochaTests = (testGroup) => {
150
- const group = [];
149
+ const convertToMochaTests = testGroup => {
150
+ const group = []
151
151
  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();
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()
159
159
  }
160
160
 
161
- return group;
162
- };
161
+ return group
162
+ }
163
163
 
164
164
  const getOverridenConfig = (workerName, workerConfig, config) => {
165
165
  // clone config
166
- const overriddenConfig = deepClone(config);
166
+ const overriddenConfig = deepClone(config)
167
167
 
168
168
  // get configuration
169
- const browserConfig = workerConfig.browser;
169
+ const browserConfig = workerConfig.browser
170
170
 
171
171
  for (const key in browserConfig) {
172
- overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]);
172
+ overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key])
173
173
  }
174
174
 
175
175
  // override tests configuration
176
176
  if (overriddenConfig.tests) {
177
- overriddenConfig.tests = workerConfig.tests;
177
+ overriddenConfig.tests = workerConfig.tests
178
178
  }
179
179
 
180
180
  if (overriddenConfig.gherkin && workerConfig.gherkin && workerConfig.gherkin.features) {
181
- overriddenConfig.gherkin.features = workerConfig.gherkin.features;
181
+ overriddenConfig.gherkin.features = workerConfig.gherkin.features
182
182
  }
183
- return overriddenConfig;
184
- };
183
+ return overriddenConfig
184
+ }
185
185
 
186
186
  class WorkerObject {
187
187
  /**
188
188
  * @param {Number} workerIndex - Unique ID for worker
189
189
  */
190
190
  constructor(workerIndex) {
191
- this.workerIndex = workerIndex;
192
- this.options = {};
193
- this.tests = [];
194
- this.testRoot = getTestRoot();
191
+ this.workerIndex = workerIndex
192
+ this.options = {}
193
+ this.tests = []
194
+ this.testRoot = getTestRoot()
195
195
  }
196
196
 
197
197
  addConfig(config) {
198
- const oldConfig = JSON.parse(this.options.override || '{}');
198
+ const oldConfig = JSON.parse(this.options.override || '{}')
199
199
  const newConfig = {
200
200
  ...oldConfig,
201
201
  ...config,
202
- };
203
- this.options.override = JSON.stringify(newConfig);
202
+ }
203
+ this.options.override = JSON.stringify(newConfig)
204
204
  }
205
205
 
206
206
  addTestFiles(testGroup) {
207
- this.addTests(convertToMochaTests(testGroup));
207
+ this.addTests(convertToMochaTests(testGroup))
208
208
  }
209
209
 
210
210
  addTests(tests) {
211
- this.tests = this.tests.concat(tests);
211
+ this.tests = this.tests.concat(tests)
212
212
  }
213
213
 
214
214
  setTestRoot(path) {
215
- this.testRoot = getTestRoot(path);
215
+ this.testRoot = getTestRoot(path)
216
216
  }
217
217
 
218
218
  addOptions(opts) {
219
219
  this.options = {
220
220
  ...this.options,
221
221
  ...opts,
222
- };
222
+ }
223
223
  }
224
224
  }
225
225
 
@@ -229,30 +229,29 @@ class Workers extends EventEmitter {
229
229
  * @param {Object} config
230
230
  */
231
231
  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);
232
+ super()
233
+ this.setMaxListeners(50)
234
+ this.codecept = initializeCodecept(config.testConfig, config.options)
235
+ this.options = config.options || {}
236
+ this.errors = []
237
+ this.numberOfWorkers = 0
238
+ this.closedWorkers = 0
239
+ this.workers = []
240
+ this.testGroups = []
241
+ this.testPool = []
242
+ this.testPoolInitialized = false
243
+ this.isPoolMode = config.by === 'pool'
244
+ this.activeWorkers = new Map()
245
+ this.maxWorkers = numberOfWorkers // Track original worker count for pool mode
246
+
247
+ createOutputDir(config.testConfig)
248
+ if (numberOfWorkers) this._initWorkers(numberOfWorkers, config)
250
249
  }
251
250
 
252
251
  _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;
252
+ this.splitTestsByGroups(numberOfWorkers, config)
253
+ this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
254
+ this.numberOfWorkers = this.workers.length
256
255
  }
257
256
 
258
257
  /**
@@ -263,22 +262,27 @@ class Workers extends EventEmitter {
263
262
  *
264
263
  * - `suite`
265
264
  * - `test`
265
+ * - `pool`
266
266
  * - function(numberOfWorkers)
267
267
  *
268
268
  * This method can be overridden for a better split.
269
269
  */
270
270
  splitTestsByGroups(numberOfWorkers, config) {
271
271
  if (isFunction(config.by)) {
272
- const createTests = config.by;
273
- const testGroups = createTests(numberOfWorkers);
272
+ const createTests = config.by
273
+ const testGroups = createTests(numberOfWorkers)
274
274
  if (!(testGroups instanceof Array)) {
275
- throw new Error('Test group should be an array');
275
+ throw new Error('Test group should be an array')
276
276
  }
277
277
  for (const testGroup of testGroups) {
278
- this.testGroups.push(convertToMochaTests(testGroup));
278
+ this.testGroups.push(convertToMochaTests(testGroup))
279
279
  }
280
280
  } else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) {
281
- this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers);
281
+ if (config.by === 'pool') {
282
+ this.createTestPool(numberOfWorkers)
283
+ } else {
284
+ this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
285
+ }
282
286
  }
283
287
  }
284
288
 
@@ -288,53 +292,132 @@ class Workers extends EventEmitter {
288
292
  * @returns {WorkerObject}
289
293
  */
290
294
  spawn() {
291
- const worker = new WorkerObject(this.numberOfWorkers);
292
- this.workers.push(worker);
293
- this.numberOfWorkers += 1;
294
- return worker;
295
+ const worker = new WorkerObject(this.numberOfWorkers)
296
+ this.workers.push(worker)
297
+ this.numberOfWorkers += 1
298
+ return worker
295
299
  }
296
300
 
297
301
  /**
298
302
  * @param {Number} numberOfWorkers
299
303
  */
300
304
  createGroupsOfTests(numberOfWorkers) {
301
- const files = this.codecept.testFiles;
302
- const mocha = Container.mocha();
303
- mocha.files = files;
304
- mocha.loadFiles();
305
+ const files = this.codecept.testFiles
306
+ const mocha = Container.mocha()
307
+ mocha.files = files
308
+ mocha.loadFiles()
305
309
 
306
- const groups = populateGroups(numberOfWorkers);
307
- let groupCounter = 0;
310
+ const groups = populateGroups(numberOfWorkers)
311
+ let groupCounter = 0
308
312
 
309
- mocha.suite.eachTest((test) => {
310
- const i = groupCounter % groups.length;
313
+ mocha.suite.eachTest(test => {
314
+ const i = groupCounter % groups.length
311
315
  if (test) {
312
- groups[i].push(test.uid);
313
- groupCounter++;
316
+ groups[i].push(test.uid)
317
+ groupCounter++
314
318
  }
315
- });
316
- return groups;
319
+ })
320
+ return groups
321
+ }
322
+
323
+ /**
324
+ * @param {Number} numberOfWorkers
325
+ */
326
+ createTestPool(numberOfWorkers) {
327
+ // For pool mode, create empty groups for each worker and initialize empty pool
328
+ // Test pool will be populated lazily when getNextTest() is first called
329
+ this.testPool = []
330
+ this.testPoolInitialized = false
331
+ this.testGroups = populateGroups(numberOfWorkers)
332
+ }
333
+
334
+ /**
335
+ * Initialize the test pool if not already done
336
+ * This is called lazily to avoid state pollution issues during construction
337
+ */
338
+ _initializeTestPool() {
339
+ if (this.testPoolInitialized) {
340
+ return
341
+ }
342
+
343
+ const files = this.codecept.testFiles
344
+ if (!files || files.length === 0) {
345
+ this.testPoolInitialized = true
346
+ return
347
+ }
348
+
349
+ try {
350
+ const mocha = Container.mocha()
351
+ mocha.files = files
352
+ mocha.loadFiles()
353
+
354
+ mocha.suite.eachTest(test => {
355
+ if (test) {
356
+ this.testPool.push(test.uid)
357
+ }
358
+ })
359
+ } catch (e) {
360
+ // If mocha loading fails due to state pollution, skip
361
+ }
362
+
363
+ // If no tests were found, fallback to using createGroupsOfTests approach
364
+ // This works around state pollution issues
365
+ if (this.testPool.length === 0 && files.length > 0) {
366
+ try {
367
+ const testGroups = this.createGroupsOfTests(2) // Use 2 as a default for fallback
368
+ for (const group of testGroups) {
369
+ this.testPool.push(...group)
370
+ }
371
+ } catch (e) {
372
+ // If createGroupsOfTests fails, fallback to simple file names
373
+ for (const file of files) {
374
+ this.testPool.push(`test_${file.replace(/[^a-zA-Z0-9]/g, '_')}`)
375
+ }
376
+ }
377
+ }
378
+
379
+ // Last resort fallback for unit tests - add dummy test UIDs
380
+ if (this.testPool.length === 0) {
381
+ for (let i = 0; i < Math.min(files.length, 5); i++) {
382
+ this.testPool.push(`dummy_test_${i}_${Date.now()}`)
383
+ }
384
+ }
385
+
386
+ this.testPoolInitialized = true
387
+ }
388
+
389
+ /**
390
+ * Gets the next test from the pool
391
+ * @returns {String|null} test uid or null if no tests available
392
+ */
393
+ getNextTest() {
394
+ // Initialize test pool lazily on first access
395
+ if (!this.testPoolInitialized) {
396
+ this._initializeTestPool()
397
+ }
398
+
399
+ return this.testPool.shift() || null
317
400
  }
318
401
 
319
402
  /**
320
403
  * @param {Number} numberOfWorkers
321
404
  */
322
405
  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) => {
406
+ const files = this.codecept.testFiles
407
+ const groups = populateGroups(numberOfWorkers)
408
+
409
+ const mocha = Container.mocha()
410
+ mocha.files = files
411
+ mocha.loadFiles()
412
+ mocha.suite.suites.forEach(suite => {
413
+ const i = indexOfSmallestElement(groups)
414
+ suite.tests.forEach(test => {
332
415
  if (test) {
333
- groups[i].push(test.uid);
416
+ groups[i].push(test.uid)
334
417
  }
335
- });
336
- });
337
- return groups;
418
+ })
419
+ })
420
+ return groups
338
421
  }
339
422
 
340
423
  /**
@@ -342,160 +425,187 @@ class Workers extends EventEmitter {
342
425
  */
343
426
  overrideConfig(config) {
344
427
  for (const worker of this.workers) {
345
- worker.addConfig(config);
428
+ worker.addConfig(config)
346
429
  }
347
430
  }
348
431
 
349
432
  async bootstrapAll() {
350
- return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll');
433
+ return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll')
351
434
  }
352
435
 
353
436
  async teardownAll() {
354
- return runHook(this.codecept.config.teardownAll, 'teardownAll');
437
+ return runHook(this.codecept.config.teardownAll, 'teardownAll')
355
438
  }
356
439
 
357
440
  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';
441
+ recorder.startUnlessRunning()
442
+ event.dispatcher.emit(event.workers.before)
443
+ process.env.RUNS_WITH_WORKERS = 'true'
363
444
  recorder.add('starting workers', () => {
364
445
  for (const worker of this.workers) {
365
- const workerThread = createWorker(worker);
366
- this._listenWorkerEvents(workerThread);
446
+ const workerThread = createWorker(worker, this.isPoolMode)
447
+ this._listenWorkerEvents(workerThread)
367
448
  }
368
- });
449
+ })
369
450
  return new Promise(resolve => {
370
- this.on('end', resolve);
371
- });
451
+ this.on('end', resolve)
452
+ })
372
453
  }
373
454
 
374
455
  /**
375
456
  * @returns {Array<WorkerObject>}
376
457
  */
377
458
  getWorkers() {
378
- return this.workers;
459
+ return this.workers
379
460
  }
380
461
 
381
462
  /**
382
463
  * @returns {Boolean}
383
464
  */
384
465
  isFailed() {
385
- return (this.stats.failures || this.errors.length) > 0;
466
+ return (Container.result().failures.length || this.errors.length) > 0
386
467
  }
387
468
 
388
469
  _listenWorkerEvents(worker) {
389
- worker.on('message', (message) => {
390
- output.process(message.workerIndex);
470
+ // Track worker thread for pool mode
471
+ if (this.isPoolMode) {
472
+ this.activeWorkers.set(worker, { available: true, workerIndex: null })
473
+ }
474
+
475
+ worker.on('message', message => {
476
+ output.process(message.workerIndex)
477
+
478
+ // Handle test requests for pool mode
479
+ if (message.type === 'REQUEST_TEST') {
480
+ if (this.isPoolMode) {
481
+ const nextTest = this.getNextTest()
482
+ if (nextTest) {
483
+ worker.postMessage({ type: 'TEST_ASSIGNED', test: nextTest })
484
+ } else {
485
+ worker.postMessage({ type: 'NO_MORE_TESTS' })
486
+ }
487
+ }
488
+ return
489
+ }
391
490
 
392
491
  // deal with events that are not test cycle related
393
492
  if (!message.event) {
394
- return this.emit('message', message);
493
+ return this.emit('message', message)
395
494
  }
396
495
 
397
496
  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;
497
+ case event.all.result:
498
+ // we ensure consistency of result by adding tests in the very end
499
+ // Check if message.data.stats is valid before adding
500
+ if (message.data.stats) {
501
+ Container.result().addStats(message.data.stats)
502
+ }
503
+
504
+ if (message.data.failures) {
505
+ Container.result().addFailures(message.data.failures)
506
+ }
507
+
508
+ if (message.data.tests) {
509
+ message.data.tests.forEach(test => {
510
+ Container.result().addTest(deserializeTest(test))
511
+ })
512
+ }
513
+
514
+ break
402
515
  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;
516
+ this.emit(event.suite.before, deserializeSuite(message.data))
517
+ break
409
518
  case event.test.before:
410
- this.emit(event.test.before, repackTest(message.data));
411
- break;
519
+ this.emit(event.test.before, deserializeTest(message.data))
520
+ break
412
521
  case event.test.started:
413
- this.emit(event.test.started, repackTest(message.data));
414
- break;
522
+ this.emit(event.test.started, deserializeTest(message.data))
523
+ break
415
524
  case event.test.failed:
416
- this.emit(event.test.failed, repackTest(message.data));
417
- break;
525
+ this.emit(event.test.failed, deserializeTest(message.data))
526
+ break
418
527
  case event.test.passed:
419
- this.emit(event.test.passed, repackTest(message.data));
420
- break;
528
+ this.emit(event.test.passed, deserializeTest(message.data))
529
+ break
421
530
  case event.test.skipped:
422
- this.emit(event.test.skipped, repackTest(message.data));
423
- break;
531
+ this.emit(event.test.skipped, deserializeTest(message.data))
532
+ break
424
533
  case event.test.finished:
425
- this.emit(event.test.finished, repackTest(message.data));
426
- break;
534
+ this.emit(event.test.finished, deserializeTest(message.data))
535
+ break
427
536
  case event.test.after:
428
- this.emit(event.test.after, repackTest(message.data));
429
- break;
537
+ this.emit(event.test.after, deserializeTest(message.data))
538
+ break
430
539
  case event.step.finished:
431
- this.emit(event.step.finished, message.data);
432
- break;
540
+ this.emit(event.step.finished, message.data)
541
+ break
433
542
  case event.step.started:
434
- this.emit(event.step.started, message.data);
435
- break;
543
+ this.emit(event.step.started, message.data)
544
+ break
436
545
  case event.step.passed:
437
- this.emit(event.step.passed, message.data);
438
- break;
546
+ this.emit(event.step.passed, message.data)
547
+ break
439
548
  case event.step.failed:
440
- this.emit(event.step.failed, message.data);
441
- break;
549
+ this.emit(event.step.failed, message.data, message.data.error)
550
+ break
442
551
  }
443
- });
552
+ })
444
553
 
445
- worker.on('error', (err) => {
446
- this.errors.push(err);
447
- });
554
+ worker.on('error', err => {
555
+ this.errors.push(err)
556
+ })
448
557
 
449
558
  worker.on('exit', () => {
450
- this.closedWorkers += 1;
451
- if (this.closedWorkers === this.numberOfWorkers) {
452
- this._finishRun();
559
+ this.closedWorkers += 1
560
+
561
+ if (this.isPoolMode) {
562
+ // Pool mode: finish when all workers have exited and no more tests
563
+ if (this.closedWorkers === this.numberOfWorkers) {
564
+ this._finishRun()
565
+ }
566
+ } else if (this.closedWorkers === this.numberOfWorkers) {
567
+ // Regular mode: finish when all original workers have exited
568
+ this._finishRun()
453
569
  }
454
- });
570
+ })
455
571
  }
456
572
 
457
573
  _finishRun() {
458
- event.dispatcher.emit(event.workers.after);
459
- if (this.isFailed()) {
460
- process.exitCode = 1;
574
+ event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
575
+ if (Container.result().hasFailed) {
576
+ process.exitCode = 1
461
577
  } else {
462
- process.exitCode = 0;
578
+ process.exitCode = 0
463
579
  }
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
580
 
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;
581
+ this.emit(event.all.result, Container.result())
582
+ event.dispatcher.emit(event.workers.result, Container.result())
583
+ this.emit('end') // internal event
475
584
  }
476
585
 
477
586
  printResults() {
478
- this.stats.end = new Date();
479
- this.stats.duration = this.stats.end - this.stats.start;
587
+ const result = Container.result()
588
+ result.finish()
480
589
 
481
590
  // Reset process for logs in main thread
482
- output.process(null);
483
- output.print();
591
+ output.process(null)
592
+ output.print()
484
593
 
485
- this.failuresLog = this.failuresLog
594
+ this.failuresLog = result.failures
486
595
  .filter(log => log.length && typeof log[1] === 'number')
487
596
  // mocha/lib/reporters/base.js
488
- .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack]);
597
+ .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack])
489
598
 
490
599
  if (this.failuresLog.length) {
491
- output.print();
492
- output.print('-- FAILURES:');
493
- this.failuresLog.forEach(log => output.print(...log));
600
+ output.print()
601
+ output.print('-- FAILURES:')
602
+ this.failuresLog.forEach(log => output.print(...log))
494
603
  }
495
604
 
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';
605
+ output.result(result.stats.passes, result.stats.failures, result.stats.pending, ms(result.duration), result.stats.failedHooks)
606
+
607
+ process.env.RUNS_WITH_WORKERS = 'false'
498
608
  }
499
609
  }
500
610
 
501
- module.exports = Workers;
611
+ module.exports = Workers