codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20

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 (209) hide show
  1. package/README.md +133 -120
  2. package/bin/codecept.js +107 -96
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/docs/webapi/click.mustache +5 -1
  6. package/lib/actor.js +71 -103
  7. package/lib/ai.js +159 -188
  8. package/lib/assert/empty.js +22 -24
  9. package/lib/assert/equal.js +30 -37
  10. package/lib/assert/error.js +14 -14
  11. package/lib/assert/include.js +43 -48
  12. package/lib/assert/throws.js +11 -11
  13. package/lib/assert/truth.js +22 -22
  14. package/lib/assert.js +20 -18
  15. package/lib/codecept.js +262 -162
  16. package/lib/colorUtils.js +50 -52
  17. package/lib/command/check.js +206 -0
  18. package/lib/command/configMigrate.js +56 -51
  19. package/lib/command/definitions.js +96 -109
  20. package/lib/command/dryRun.js +77 -79
  21. package/lib/command/generate.js +234 -194
  22. package/lib/command/gherkin/init.js +42 -33
  23. package/lib/command/gherkin/snippets.js +76 -74
  24. package/lib/command/gherkin/steps.js +20 -17
  25. package/lib/command/info.js +74 -38
  26. package/lib/command/init.js +301 -290
  27. package/lib/command/interactive.js +41 -32
  28. package/lib/command/list.js +28 -27
  29. package/lib/command/run-multiple/chunk.js +51 -48
  30. package/lib/command/run-multiple/collection.js +5 -5
  31. package/lib/command/run-multiple/run.js +5 -1
  32. package/lib/command/run-multiple.js +97 -97
  33. package/lib/command/run-rerun.js +19 -25
  34. package/lib/command/run-workers.js +68 -92
  35. package/lib/command/run.js +39 -27
  36. package/lib/command/utils.js +80 -64
  37. package/lib/command/workers/runTests.js +388 -226
  38. package/lib/config.js +109 -50
  39. package/lib/container.js +641 -261
  40. package/lib/data/context.js +60 -61
  41. package/lib/data/dataScenarioConfig.js +47 -47
  42. package/lib/data/dataTableArgument.js +32 -32
  43. package/lib/data/table.js +22 -22
  44. package/lib/effects.js +307 -0
  45. package/lib/element/WebElement.js +327 -0
  46. package/lib/els.js +160 -0
  47. package/lib/event.js +173 -163
  48. package/lib/globals.js +141 -0
  49. package/lib/heal.js +89 -85
  50. package/lib/helper/AI.js +131 -41
  51. package/lib/helper/ApiDataFactory.js +107 -75
  52. package/lib/helper/Appium.js +542 -404
  53. package/lib/helper/FileSystem.js +100 -79
  54. package/lib/helper/GraphQL.js +44 -43
  55. package/lib/helper/GraphQLDataFactory.js +52 -52
  56. package/lib/helper/JSONResponse.js +126 -88
  57. package/lib/helper/Mochawesome.js +54 -29
  58. package/lib/helper/Playwright.js +2547 -1316
  59. package/lib/helper/Puppeteer.js +1578 -1181
  60. package/lib/helper/REST.js +209 -68
  61. package/lib/helper/WebDriver.js +1482 -1342
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
  71. package/lib/helper/extras/Popup.js +22 -22
  72. package/lib/helper/extras/React.js +27 -28
  73. package/lib/helper/network/actions.js +36 -42
  74. package/lib/helper/network/utils.js +78 -84
  75. package/lib/helper/scripts/blurElement.js +5 -5
  76. package/lib/helper/scripts/focusElement.js +5 -5
  77. package/lib/helper/scripts/highlightElement.js +8 -8
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -3
  80. package/lib/history.js +23 -19
  81. package/lib/hooks.js +8 -8
  82. package/lib/html.js +94 -104
  83. package/lib/index.js +38 -27
  84. package/lib/listener/config.js +30 -23
  85. package/lib/listener/emptyRun.js +54 -0
  86. package/lib/listener/enhancedGlobalRetry.js +110 -0
  87. package/lib/listener/exit.js +16 -18
  88. package/lib/listener/globalRetry.js +70 -0
  89. package/lib/listener/globalTimeout.js +181 -0
  90. package/lib/listener/helpers.js +76 -51
  91. package/lib/listener/mocha.js +10 -11
  92. package/lib/listener/result.js +11 -0
  93. package/lib/listener/retryEnhancer.js +85 -0
  94. package/lib/listener/steps.js +71 -59
  95. package/lib/listener/store.js +20 -0
  96. package/lib/locator.js +214 -197
  97. package/lib/mocha/asyncWrapper.js +274 -0
  98. package/lib/mocha/bdd.js +167 -0
  99. package/lib/mocha/cli.js +341 -0
  100. package/lib/mocha/factory.js +163 -0
  101. package/lib/mocha/featureConfig.js +89 -0
  102. package/lib/mocha/gherkin.js +231 -0
  103. package/lib/mocha/hooks.js +121 -0
  104. package/lib/mocha/index.js +21 -0
  105. package/lib/mocha/inject.js +46 -0
  106. package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
  107. package/lib/mocha/suite.js +89 -0
  108. package/lib/mocha/test.js +184 -0
  109. package/lib/mocha/types.d.ts +42 -0
  110. package/lib/mocha/ui.js +242 -0
  111. package/lib/output.js +141 -71
  112. package/lib/parser.js +47 -44
  113. package/lib/pause.js +173 -145
  114. package/lib/plugin/analyze.js +403 -0
  115. package/lib/plugin/{autoLogin.js → auth.js} +178 -79
  116. package/lib/plugin/autoDelay.js +36 -40
  117. package/lib/plugin/coverage.js +131 -78
  118. package/lib/plugin/customLocator.js +22 -21
  119. package/lib/plugin/customReporter.js +53 -0
  120. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  121. package/lib/plugin/heal.js +101 -110
  122. package/lib/plugin/htmlReporter.js +3648 -0
  123. package/lib/plugin/pageInfo.js +140 -0
  124. package/lib/plugin/pauseOnFail.js +12 -11
  125. package/lib/plugin/retryFailedStep.js +82 -47
  126. package/lib/plugin/screenshotOnFail.js +111 -92
  127. package/lib/plugin/stepByStepReport.js +159 -101
  128. package/lib/plugin/stepTimeout.js +20 -25
  129. package/lib/plugin/subtitles.js +38 -38
  130. package/lib/recorder.js +193 -130
  131. package/lib/rerun.js +94 -49
  132. package/lib/result.js +238 -0
  133. package/lib/retryCoordinator.js +207 -0
  134. package/lib/secret.js +20 -18
  135. package/lib/session.js +95 -89
  136. package/lib/step/base.js +239 -0
  137. package/lib/step/comment.js +10 -0
  138. package/lib/step/config.js +50 -0
  139. package/lib/step/func.js +46 -0
  140. package/lib/step/helper.js +50 -0
  141. package/lib/step/meta.js +99 -0
  142. package/lib/step/record.js +74 -0
  143. package/lib/step/retry.js +11 -0
  144. package/lib/step/section.js +55 -0
  145. package/lib/step.js +18 -329
  146. package/lib/steps.js +54 -0
  147. package/lib/store.js +38 -7
  148. package/lib/template/heal.js +3 -12
  149. package/lib/template/prompts/generatePageObject.js +31 -0
  150. package/lib/template/prompts/healStep.js +13 -0
  151. package/lib/template/prompts/writeStep.js +9 -0
  152. package/lib/test-server.js +334 -0
  153. package/lib/timeout.js +60 -0
  154. package/lib/transform.js +8 -8
  155. package/lib/translation.js +34 -21
  156. package/lib/utils/loaderCheck.js +124 -0
  157. package/lib/utils/mask_data.js +47 -0
  158. package/lib/utils/typescript.js +237 -0
  159. package/lib/utils.js +411 -228
  160. package/lib/workerStorage.js +37 -34
  161. package/lib/workers.js +532 -296
  162. package/package.json +124 -95
  163. package/translations/de-DE.js +5 -3
  164. package/translations/fr-FR.js +5 -4
  165. package/translations/index.js +22 -12
  166. package/translations/it-IT.js +4 -3
  167. package/translations/ja-JP.js +4 -3
  168. package/translations/nl-NL.js +76 -0
  169. package/translations/pl-PL.js +4 -3
  170. package/translations/pt-BR.js +4 -3
  171. package/translations/ru-RU.js +4 -3
  172. package/translations/utils.js +10 -0
  173. package/translations/zh-CN.js +4 -3
  174. package/translations/zh-TW.js +4 -3
  175. package/typings/index.d.ts +546 -185
  176. package/typings/promiseBasedTypes.d.ts +150 -875
  177. package/typings/types.d.ts +547 -992
  178. package/lib/cli.js +0 -249
  179. package/lib/dirname.js +0 -5
  180. package/lib/helper/Expect.js +0 -425
  181. package/lib/helper/ExpectHelper.js +0 -399
  182. package/lib/helper/MockServer.js +0 -223
  183. package/lib/helper/Nightmare.js +0 -1411
  184. package/lib/helper/Protractor.js +0 -1835
  185. package/lib/helper/SoftExpectHelper.js +0 -381
  186. package/lib/helper/TestCafe.js +0 -1410
  187. package/lib/helper/clientscripts/nightmare.js +0 -213
  188. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  189. package/lib/helper/testcafe/testcafe-utils.js +0 -63
  190. package/lib/interfaces/bdd.js +0 -98
  191. package/lib/interfaces/featureConfig.js +0 -69
  192. package/lib/interfaces/gherkin.js +0 -195
  193. package/lib/listener/artifacts.js +0 -19
  194. package/lib/listener/retry.js +0 -68
  195. package/lib/listener/timeout.js +0 -109
  196. package/lib/mochaFactory.js +0 -110
  197. package/lib/plugin/allure.js +0 -15
  198. package/lib/plugin/commentStep.js +0 -136
  199. package/lib/plugin/debugErrors.js +0 -67
  200. package/lib/plugin/eachElement.js +0 -127
  201. package/lib/plugin/fakerTransform.js +0 -49
  202. package/lib/plugin/retryTo.js +0 -121
  203. package/lib/plugin/selenoid.js +0 -371
  204. package/lib/plugin/standardActingHelpers.js +0 -9
  205. package/lib/plugin/tryTo.js +0 -105
  206. package/lib/plugin/wdio.js +0 -246
  207. package/lib/scenario.js +0 -222
  208. package/lib/ui.js +0 -238
  209. package/lib/within.js +0 -70
@@ -1,292 +1,454 @@
1
- import tty from 'tty';
2
- import { parentPort, workerData } from 'worker_threads';
3
- import * as event from '../../event.js';
4
- import Container from '../../container.js';
5
- import { getConfig } from '../utils.js';
6
- import { deepMerge, tryOrDefault } from '../../utils.js';
7
- import Codecept from '../../codecept.js';
1
+ import tty from 'tty'
8
2
 
9
3
  if (!tty.getWindowSize) {
10
4
  // this is really old method, long removed from Node, but Mocha
11
5
  // reporters fall back on it if they cannot use `process.stdout.getWindowSize`
12
6
  // we need to polyfill it.
13
- tty.getWindowSize = () => [40, 80];
7
+ tty.getWindowSize = () => [40, 80]
14
8
  }
15
9
 
16
- // eslint-disable-next-line no-unused-vars
17
- let stdout = '';
18
- /* eslint-enable no-unused-vars */
19
- const stderr = '';
10
+ import { parentPort, workerData } from 'worker_threads'
20
11
 
21
- // Requiring of Codecept need to be after tty.getWindowSize is available.
22
- // const Codecept = importSync(process.env.CODECEPT_CLASS_PATH || '../../codecept.js');
12
+ // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
13
+ // These will be imported dynamically when needed
14
+ let event, container, Codecept, getConfig, tryOrDefault, deepMerge
23
15
 
24
- const {
25
- options, tests, testRoot, workerIndex,
26
- } = workerData;
16
+ let stdout = ''
17
+
18
+ const stderr = ''
19
+
20
+ const { options, tests, testRoot, workerIndex, poolMode } = workerData
27
21
 
28
22
  // hide worker output
29
- if (!options.debug && !options.verbose) process.stdout.write = (string) => { stdout += string; return true; };
23
+ // In pool mode, only suppress output if debug is NOT enabled
24
+ // In regular mode, hide result output but allow step output in verbose/debug
25
+ if (poolMode && !options.debug) {
26
+ // In pool mode without debug, allow test names and important output but suppress verbose details
27
+ const originalWrite = process.stdout.write
28
+ process.stdout.write = string => {
29
+ // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
30
+ if (
31
+ string.includes('✔') ||
32
+ string.includes('✖') ||
33
+ string.includes('Scenario Steps:') ||
34
+ string.includes('◯ Scenario Steps:') ||
35
+ string.includes('-- FAILURES:') ||
36
+ string.includes('AssertionError:') ||
37
+ string.includes('Feature(')
38
+ ) {
39
+ return originalWrite.call(process.stdout, string)
40
+ }
41
+ // Suppress result summaries to avoid duplicates
42
+ if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('◯ File:')) {
43
+ return true
44
+ }
45
+ return originalWrite.call(process.stdout, string)
46
+ }
47
+ } else if (!poolMode && !options.debug && !options.verbose) {
48
+ process.stdout.write = string => {
49
+ stdout += string
50
+ return true
51
+ }
52
+ } else {
53
+ // In verbose/debug mode for test/suite modes, show step details
54
+ // but suppress individual worker result summaries to avoid duplicate output
55
+ const originalWrite = process.stdout.write
56
+ const originalConsoleLog = console.log
57
+
58
+ process.stdout.write = string => {
59
+ // Suppress individual worker result summaries and failure reports
60
+ if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:') || string.includes('◯ Scenario Steps:')) {
61
+ return true
62
+ }
63
+ return originalWrite.call(process.stdout, string)
64
+ }
30
65
 
31
- const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {});
66
+ // Override console.log to catch result summaries
67
+ console.log = (...args) => {
68
+ const fullMessage = args.join(' ')
69
+ if (fullMessage.includes(' FAIL |') || fullMessage.includes(' OK |') || fullMessage.includes('-- FAILURES:')) {
70
+ return
71
+ }
72
+ return originalConsoleLog.apply(console, args)
73
+ }
74
+ }
32
75
 
33
- // important deep merge so dynamic things e.g. functions on config are not overridden
34
- const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs);
76
+ // Declare codecept and mocha at module level so they can be accessed by functions
77
+ let codecept
78
+ let mocha
79
+ let initPromise
80
+ let config
35
81
 
36
82
  // Load test and run
37
- const codecept = new Codecept(config, options);
38
- codecept.init(testRoot);
39
- codecept.loadTests();
40
- const mocha = Container.mocha();
41
- filterTests();
42
-
43
- (async function () {
44
- if (mocha.suite.total()) {
45
- await runTests();
83
+ initPromise = (async function () {
84
+ try {
85
+ // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
+ const eventModule = await import('../../event.js')
87
+ const containerModule = await import('../../container.js')
88
+ const utilsModule = await import('../utils.js')
89
+ const coreUtilsModule = await import('../../utils.js')
90
+ const CodeceptModule = await import('../../codecept.js')
91
+
92
+ event = eventModule.default
93
+ container = containerModule.default
94
+ getConfig = utilsModule.getConfig
95
+ tryOrDefault = coreUtilsModule.tryOrDefault
96
+ deepMerge = coreUtilsModule.deepMerge
97
+ Codecept = CodeceptModule.default
98
+
99
+ const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
+
101
+ // IMPORTANT: await is required here since getConfig is async
102
+ const baseConfig = await getConfig(options.config || testRoot)
103
+
104
+ // important deep merge so dynamic things e.g. functions on config are not overridden
105
+ config = deepMerge(baseConfig, overrideConfigs)
106
+
107
+ codecept = new Codecept(config, options)
108
+ await codecept.init(testRoot)
109
+ codecept.loadTests()
110
+ mocha = container.mocha()
111
+
112
+ if (poolMode) {
113
+ // In pool mode, don't filter tests upfront - wait for assignments
114
+ // We'll reload test files fresh for each test request
115
+ } else {
116
+ // Legacy mode - filter tests upfront
117
+ filterTests()
118
+ }
119
+
120
+ // run tests
121
+ if (poolMode) {
122
+ await runPoolTests()
123
+ } else if (mocha.suite.total()) {
124
+ await runTests()
125
+ } else {
126
+ // No tests to run, close the worker
127
+ parentPort?.close()
128
+ }
129
+ } catch (err) {
130
+ console.error('Error in worker initialization:', err)
131
+ process.exit(1)
46
132
  }
47
- }());
133
+ })()
134
+
135
+ let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
48
136
 
49
137
  async function runTests() {
50
138
  try {
51
- await codecept.bootstrap();
139
+ await codecept.bootstrap()
52
140
  } catch (err) {
53
- throw new Error(`Error while running bootstrap file :${err}`);
141
+ throw new Error(`Error while running bootstrap file :${err}`)
54
142
  }
55
- listenToParentThread();
56
- initializeListeners();
57
- disablePause();
143
+ listenToParentThread()
144
+ initializeListeners()
145
+ disablePause()
58
146
  try {
59
- await codecept.run();
147
+ await codecept.run()
60
148
  } finally {
61
- await codecept.teardown();
149
+ await codecept.teardown()
62
150
  }
63
151
  }
64
152
 
65
- function filterTests() {
66
- const files = codecept.testFiles;
67
- mocha.files = files;
68
- mocha.loadFiles();
69
-
70
- for (const suite of mocha.suite.suites) {
71
- suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0);
72
- }
73
- }
74
-
75
- function initializeListeners() {
76
- function simplifyError(error) {
77
- if (error) {
78
- const {
79
- stack,
80
- uncaught,
81
- message,
82
- actual,
83
- expected,
84
- } = error;
85
-
86
- return {
87
- stack,
88
- uncaught,
89
- message,
90
- actual,
91
- expected,
92
- };
93
- }
94
-
95
- return null;
153
+ async function runPoolTests() {
154
+ try {
155
+ await codecept.bootstrap()
156
+ } catch (err) {
157
+ throw new Error(`Error while running bootstrap file :${err}`)
96
158
  }
97
- function simplifyTest(test, err = null) {
98
- test = { ...test };
99
159
 
100
- if (test.start && !test.duration) {
101
- const end = new Date();
102
- test.duration = end - test.start;
103
- }
160
+ initializeListeners()
161
+ disablePause()
162
+
163
+ // Emit event.all.before once at the start of pool mode
164
+ event.dispatcher.emit(event.all.before, codecept)
165
+
166
+ // Accumulate results across all tests in pool mode
167
+ let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
168
+ let allTests = []
169
+ let allFailures = []
170
+ let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
171
+
172
+ // Keep requesting tests until no more available
173
+ while (true) {
174
+ // Request a test assignment and wait for response
175
+ const testResult = await new Promise((resolve, reject) => {
176
+ // Set up pool mode message handler FIRST before sending request
177
+ const messageHandler = async eventData => {
178
+ // Remove handler immediately to prevent duplicate processing
179
+ parentPort?.off('message', messageHandler)
180
+
181
+ if (eventData.type === 'TEST_ASSIGNED') {
182
+ // In pool mode with ESM, we receive test FILE paths instead of UIDs
183
+ // because UIDs are not stable across different mocha instances
184
+ const testIdentifier = eventData.test
185
+
186
+ try {
187
+ // Create a fresh Mocha instance for each test file
188
+ container.createMocha()
189
+ const mocha = container.mocha()
190
+
191
+ // Load only the assigned test file
192
+ mocha.files = [testIdentifier]
193
+ mocha.loadFiles()
194
+
195
+ try {
196
+ require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Loaded ${testIdentifier}, tests: ${mocha.suite.total()}\n`)
197
+ } catch (e) { /* ignore */ }
198
+
199
+ if (mocha.suite.total() > 0) {
200
+ // Run only the tests in the current mocha suite
201
+ // Don't use codecept.run() as it overwrites mocha.files with ALL test files
202
+ await new Promise((resolve, reject) => {
203
+ mocha.run(() => {
204
+ try {
205
+ require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Finished ${testIdentifier}\n`)
206
+ } catch (e) { /* ignore */ }
207
+ resolve()
208
+ })
209
+ })
210
+
211
+ // Get the results from this specific test run
212
+ const result = container.result()
213
+ const currentStats = result.stats || {}
214
+
215
+ // Calculate the difference from previous accumulated stats
216
+ const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes)
217
+ const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures)
218
+ const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests)
219
+ const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending)
220
+ const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks)
221
+
222
+ // Add only the new results
223
+ consolidatedStats.passes += newPasses
224
+ consolidatedStats.failures += newFailures
225
+ consolidatedStats.tests += newTests
226
+ consolidatedStats.pending += newPending
227
+ consolidatedStats.failedHooks += newFailedHooks
228
+
229
+ // Update previous stats for next comparison
230
+ previousStats = { ...currentStats }
231
+
232
+ // Add new failures to consolidated collections
233
+ if (result.failures && result.failures.length > allFailures.length) {
234
+ const newFailures = result.failures.slice(allFailures.length)
235
+ allFailures.push(...newFailures)
236
+ }
237
+ }
238
+
239
+ // Signal test completed
240
+ resolve('TEST_COMPLETED')
241
+ } catch (err) {
242
+ reject(err)
243
+ }
244
+ } else if (eventData.type === 'NO_MORE_TESTS') {
245
+ // No tests available, exit worker
246
+ resolve('NO_MORE_TESTS')
247
+ } else {
248
+ // Handle other message types (support messages, etc.)
249
+ container.append({ support: eventData.data })
250
+ // Don't re-add handler - each test request creates its own one-time handler
251
+ }
252
+ }
104
253
 
105
- if (test.err) {
106
- err = simplifyError(test.err);
107
- test.status = 'failed';
108
- } else if (err) {
109
- err = simplifyError(err);
110
- test.status = 'failed';
111
- }
112
- const parent = {};
113
- if (test.parent) {
114
- parent.title = test.parent.title;
115
- }
254
+ // Set up handler BEFORE sending request to avoid race condition
255
+ parentPort?.on('message', messageHandler)
256
+
257
+ // Now send the request
258
+ sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
259
+ })
116
260
 
117
- if (test.opts) {
118
- Object.keys(test.opts).forEach(k => {
119
- if (typeof test.opts[k] === 'object') delete test.opts[k];
120
- if (typeof test.opts[k] === 'function') delete test.opts[k];
121
- });
261
+ // Exit if no more tests
262
+ if (testResult === 'NO_MORE_TESTS') {
263
+ break
122
264
  }
123
-
124
- return {
125
- opts: test.opts || {},
126
- tags: test.tags || [],
127
- uid: test.uid,
128
- workerIndex,
129
- retries: test._retries,
130
- title: test.title,
131
- status: test.status,
132
- duration: test.duration || 0,
133
- err,
134
- parent,
135
- steps: test.steps && test.steps.length > 0 ? simplifyStepsInTestObject(test.steps, err) : [],
136
- };
137
265
  }
138
266
 
139
- function simplifyStepsInTestObject(steps, err) {
140
- steps = [...steps];
141
- const _steps = [];
142
-
143
- for (step of steps) {
144
- const _args = [];
145
-
146
- if (step.args) {
147
- for (const arg of step.args) {
148
- // check if arg is a JOI object
149
- if (arg && arg.$_root) {
150
- _args.push(JSON.stringify(arg).slice(0, 300));
151
- // check if arg is a function
152
- } else if (arg && typeof arg === 'function') {
153
- _args.push(arg.name);
154
- } else {
155
- _args.push(arg);
156
- }
157
- }
158
- }
267
+ // Emit event.all.after once at the end of pool mode
268
+ event.dispatcher.emit(event.all.after, codecept)
159
269
 
160
- _steps.push({
161
- actor: step.actor,
162
- name: step.name,
163
- status: step.status,
164
- args: JSON.stringify(_args),
165
- startedAt: step.startedAt,
166
- startTime: step.startTime,
167
- endTime: step.endTime,
168
- finishedAt: step.finishedAt,
169
- duration: step.duration,
170
- err,
171
- });
172
- }
270
+ try {
271
+ await codecept.teardown()
272
+ } catch (err) {
273
+ // Log teardown errors but don't fail
274
+ console.error('Teardown error:', err)
275
+ }
173
276
 
174
- return _steps;
277
+ // Send final consolidated results for the entire worker
278
+ const finalResult = {
279
+ hasFailed: consolidatedStats.failures > 0,
280
+ stats: consolidatedStats,
281
+ duration: 0, // Pool mode doesn't track duration per worker
282
+ tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient
283
+ failures: allFailures, // Include all failures for error reporting
175
284
  }
176
285
 
177
- function simplifyStep(step, err = null) {
178
- step = { ...step };
286
+ sendToParentThread({ event: event.all.after, workerIndex, data: finalResult })
287
+ sendToParentThread({ event: event.all.result, workerIndex, data: finalResult })
179
288
 
180
- if (step.startTime && !step.duration) {
181
- const end = new Date();
182
- step.duration = end - step.startTime;
183
- }
289
+ // Add longer delay to ensure messages are delivered before closing
290
+ await new Promise(resolve => setTimeout(resolve, 100))
184
291
 
185
- if (step.err) {
186
- err = simplifyError(step.err);
187
- step.status = 'failed';
188
- } else if (err) {
189
- err = simplifyError(err);
190
- step.status = 'failed';
191
- }
292
+ // Close worker thread when pool mode is complete
293
+ parentPort?.close()
294
+ }
192
295
 
193
- const parent = {};
194
- if (step.metaStep) {
195
- parent.title = step.metaStep.actor;
296
+ function filterTestById(testUid) {
297
+ // In pool mode with ESM, test files are already loaded once at initialization
298
+ // We just need to filter the existing mocha suite to only include the target test
299
+
300
+ // Get the existing mocha instance
301
+ const mocha = container.mocha()
302
+
303
+ // Save reference to all suites before clearing
304
+ const allSuites = [...mocha.suite.suites]
305
+
306
+ // Clear suites and tests but preserve other mocha settings
307
+ mocha.suite.suites = []
308
+ mocha.suite.tests = []
309
+
310
+ // Find and add only the suite containing our target test
311
+ let foundTest = false
312
+ for (const suite of allSuites) {
313
+ const originalTests = [...suite.tests]
314
+
315
+ // Check if this suite has our target test
316
+ const targetTest = originalTests.find(test => test.uid === testUid)
317
+
318
+ if (targetTest) {
319
+ // Create a filtered suite with only the target test
320
+ suite.tests = [targetTest]
321
+ mocha.suite.suites.push(suite)
322
+ foundTest = true
323
+ break // Only include one test
196
324
  }
325
+ }
197
326
 
198
- if (step.opts) {
199
- Object.keys(step.opts).forEach(k => {
200
- if (typeof step.opts[k] === 'object') delete step.opts[k];
201
- if (typeof step.opts[k] === 'function') delete step.opts[k];
202
- });
203
- }
327
+ if (!foundTest) {
328
+ console.error(`WARNING: Test with UID ${testUid} not found in mocha suites`)
329
+ }
330
+ }
204
331
 
205
- return {
206
- opts: step.opts || {},
207
- workerIndex,
208
- title: step.name,
209
- status: step.status,
210
- duration: step.duration || 0,
211
- err,
212
- parent,
213
- test: simplifyTest(step.test),
214
- };
332
+ function filterTests() {
333
+ const files = codecept.testFiles
334
+ mocha.files = files
335
+ mocha.loadFiles()
336
+
337
+ for (const suite of mocha.suite.suites) {
338
+ suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
215
339
  }
340
+ }
216
341
 
217
- collectStats();
342
+ function initializeListeners() {
218
343
  // suite
219
- event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: simplifyTest(suite) }));
220
- event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: simplifyTest(suite) }));
344
+ event.dispatcher.on(event.suite.before, suite => safelySendToParent({ event: event.suite.before, workerIndex, data: suite.simplify() }))
345
+ event.dispatcher.on(event.suite.after, suite => safelySendToParent({ event: event.suite.after, workerIndex, data: suite.simplify() }))
221
346
 
222
347
  // calculate duration
223
- event.dispatcher.on(event.test.started, test => test.start = new Date());
348
+ event.dispatcher.on(event.test.started, test => (test.start = new Date()))
224
349
 
225
350
  // tests
226
- event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: simplifyTest(test) }));
227
- event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: simplifyTest(test) }));
351
+ event.dispatcher.on(event.test.before, test => safelySendToParent({ event: event.test.before, workerIndex, data: test.simplify() }))
352
+ event.dispatcher.on(event.test.after, test => safelySendToParent({ event: event.test.after, workerIndex, data: test.simplify() }))
228
353
  // we should force-send correct errors to prevent race condition
229
- event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: simplifyTest(test, err) }));
230
- event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: simplifyTest(test, err) }));
231
- event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: simplifyTest(test, err) }));
232
- event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: simplifyTest(test) }));
233
- event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: simplifyTest(test) }));
354
+ event.dispatcher.on(event.test.finished, (test, err) => {
355
+ const simplifiedData = test.simplify()
356
+ const serializableErr = serializeError(err)
357
+ safelySendToParent({ event: event.test.finished, workerIndex, data: { ...simplifiedData, err: serializableErr } })
358
+ })
359
+ event.dispatcher.on(event.test.failed, (test, err, hookName) => {
360
+ const simplifiedData = test.simplify()
361
+ const serializableErr = serializeError(err)
362
+ // Include hookName to identify hook failures
363
+ safelySendToParent({ event: event.test.failed, workerIndex, data: { ...simplifiedData, err: serializableErr, hookName } })
364
+ })
365
+ event.dispatcher.on(event.test.passed, (test, err) => safelySendToParent({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } }))
366
+ event.dispatcher.on(event.test.started, test => safelySendToParent({ event: event.test.started, workerIndex, data: test.simplify() }))
367
+ event.dispatcher.on(event.test.skipped, test => safelySendToParent({ event: event.test.skipped, workerIndex, data: test.simplify() }))
234
368
 
235
369
  // steps
236
- event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: simplifyStep(step) }));
237
- event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: simplifyStep(step) }));
238
- event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: simplifyStep(step) }));
239
- event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: simplifyStep(step) }));
240
-
241
- event.dispatcher.on(event.hook.failed, (test, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: simplifyTest(test, err) }));
242
- event.dispatcher.on(event.hook.passed, (test, err) => sendToParentThread({ event: event.hook.passed, workerIndex, data: simplifyTest(test, err) }));
243
- event.dispatcher.on(event.all.failures, (data) => sendToParentThread({ event: event.all.failures, workerIndex, data }));
244
-
245
- // all
246
- event.dispatcher.once(event.all.result, () => parentPort.close());
370
+ event.dispatcher.on(event.step.finished, step => safelySendToParent({ event: event.step.finished, workerIndex, data: step.simplify() }))
371
+ event.dispatcher.on(event.step.started, step => safelySendToParent({ event: event.step.started, workerIndex, data: step.simplify() }))
372
+ event.dispatcher.on(event.step.passed, step => safelySendToParent({ event: event.step.passed, workerIndex, data: step.simplify() }))
373
+ event.dispatcher.on(event.step.failed, step => safelySendToParent({ event: event.step.failed, workerIndex, data: step.simplify() }))
374
+
375
+ event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } }))
376
+ event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
377
+ event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() }))
378
+
379
+ if (!poolMode) {
380
+ // In regular mode, close worker after all tests are complete
381
+ event.dispatcher.once(event.all.after, () => {
382
+ sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
383
+ })
384
+ // all
385
+ event.dispatcher.once(event.all.result, () => {
386
+ sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
387
+ parentPort?.close()
388
+ })
389
+ } else {
390
+ // In pool mode, don't send result events for individual tests
391
+ // Results will be sent once when the worker completes all tests
392
+ }
247
393
  }
248
394
 
249
395
  function disablePause() {
250
- global.pause = () => {};
396
+ global.pause = () => {}
251
397
  }
252
398
 
253
- function collectStats() {
254
- const stats = {
255
- passes: 0,
256
- failures: 0,
257
- skipped: 0,
258
- tests: 0,
259
- pending: 0,
260
- };
261
- event.dispatcher.on(event.test.skipped, () => {
262
- stats.skipped++;
263
- });
264
- event.dispatcher.on(event.test.passed, () => {
265
- stats.passes++;
266
- });
267
- event.dispatcher.on(event.test.failed, (test) => {
268
- if (test.ctx._runnable.title.includes('hook: AfterSuite')) {
269
- stats.failedHooks += 1;
399
+ function serializeError(err) {
400
+ if (!err) return null
401
+ try {
402
+ return {
403
+ message: err.message,
404
+ stack: err.stack,
405
+ name: err.name,
406
+ actual: err.actual,
407
+ expected: err.expected,
270
408
  }
271
- stats.failures++;
272
- });
273
- event.dispatcher.on(event.test.skipped, () => {
274
- stats.pending++;
275
- });
276
- event.dispatcher.on(event.test.finished, () => {
277
- stats.tests++;
278
- });
279
- event.dispatcher.once(event.all.after, () => {
280
- sendToParentThread({ event: event.all.after, data: stats });
281
- });
409
+ } catch {
410
+ return { message: 'Error could not be serialized', name: 'Error' }
411
+ }
412
+ }
413
+
414
+ function safelySendToParent(data) {
415
+ try {
416
+ parentPort?.postMessage(data)
417
+ } catch (cloneError) {
418
+ // Fallback for non-serializable data
419
+ const fallbackData = { ...data }
420
+
421
+ // Try to serialize error objects if present
422
+ if (fallbackData.data && fallbackData.data.err) {
423
+ fallbackData.data.err = serializeError(fallbackData.data.err)
424
+ }
425
+
426
+ // If still fails, send minimal data
427
+ try {
428
+ parentPort?.postMessage(fallbackData)
429
+ } catch (finalError) {
430
+ parentPort?.postMessage({
431
+ event: data.event,
432
+ workerIndex,
433
+ data: {
434
+ title: fallbackData.data?.title || 'Unknown',
435
+ state: fallbackData.data?.state || 'error',
436
+ err: { message: 'Data could not be serialized' },
437
+ },
438
+ })
439
+ }
440
+ }
282
441
  }
283
442
 
284
443
  function sendToParentThread(data) {
285
- parentPort.postMessage(data);
444
+ parentPort?.postMessage(data)
286
445
  }
287
446
 
288
447
  function listenToParentThread() {
289
- parentPort.on('message', (eventData) => {
290
- Container.append({ support: eventData.data });
291
- });
448
+ if (!poolMode) {
449
+ parentPort?.on('message', eventData => {
450
+ container.append({ support: eventData.data })
451
+ })
452
+ }
453
+ // In pool mode, message handling is done in runPoolTests()
292
454
  }