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