codeceptjs 3.7.6-beta.4 → 4.0.0-beta.10.esm-aria

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +1 -3
  2. package/bin/codecept.js +51 -53
  3. package/bin/test-server.js +14 -3
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/actor.js +15 -11
  6. package/lib/ai.js +72 -107
  7. package/lib/assert/empty.js +9 -8
  8. package/lib/assert/equal.js +15 -17
  9. package/lib/assert/error.js +2 -2
  10. package/lib/assert/include.js +9 -11
  11. package/lib/assert/throws.js +1 -1
  12. package/lib/assert/truth.js +8 -5
  13. package/lib/assert.js +18 -18
  14. package/lib/codecept.js +102 -75
  15. package/lib/colorUtils.js +48 -50
  16. package/lib/command/check.js +32 -27
  17. package/lib/command/configMigrate.js +11 -10
  18. package/lib/command/definitions.js +16 -10
  19. package/lib/command/dryRun.js +16 -16
  20. package/lib/command/generate.js +62 -27
  21. package/lib/command/gherkin/init.js +36 -38
  22. package/lib/command/gherkin/snippets.js +14 -14
  23. package/lib/command/gherkin/steps.js +21 -18
  24. package/lib/command/info.js +8 -8
  25. package/lib/command/init.js +36 -29
  26. package/lib/command/interactive.js +11 -10
  27. package/lib/command/list.js +10 -9
  28. package/lib/command/run-multiple/chunk.js +5 -5
  29. package/lib/command/run-multiple/collection.js +5 -5
  30. package/lib/command/run-multiple/run.js +3 -3
  31. package/lib/command/run-multiple.js +16 -13
  32. package/lib/command/run-rerun.js +6 -7
  33. package/lib/command/run-workers.js +24 -9
  34. package/lib/command/run.js +23 -8
  35. package/lib/command/utils.js +20 -18
  36. package/lib/command/workers/runTests.js +197 -114
  37. package/lib/config.js +124 -51
  38. package/lib/container.js +438 -87
  39. package/lib/data/context.js +6 -5
  40. package/lib/data/dataScenarioConfig.js +1 -1
  41. package/lib/data/dataTableArgument.js +1 -1
  42. package/lib/data/table.js +1 -1
  43. package/lib/effects.js +94 -10
  44. package/lib/element/WebElement.js +2 -2
  45. package/lib/els.js +11 -9
  46. package/lib/event.js +11 -10
  47. package/lib/globals.js +141 -0
  48. package/lib/heal.js +12 -12
  49. package/lib/helper/AI.js +11 -11
  50. package/lib/helper/ApiDataFactory.js +50 -19
  51. package/lib/helper/Appium.js +19 -27
  52. package/lib/helper/FileSystem.js +32 -12
  53. package/lib/helper/GraphQL.js +3 -3
  54. package/lib/helper/GraphQLDataFactory.js +4 -4
  55. package/lib/helper/JSONResponse.js +25 -29
  56. package/lib/helper/Mochawesome.js +7 -4
  57. package/lib/helper/Playwright.js +902 -164
  58. package/lib/helper/Puppeteer.js +383 -76
  59. package/lib/helper/REST.js +29 -12
  60. package/lib/helper/WebDriver.js +268 -61
  61. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  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 +18 -9
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
  71. package/lib/helper/extras/Popup.js +1 -1
  72. package/lib/helper/extras/React.js +29 -30
  73. package/lib/helper/network/actions.js +29 -44
  74. package/lib/helper/network/utils.js +76 -83
  75. package/lib/helper/scripts/blurElement.js +6 -6
  76. package/lib/helper/scripts/focusElement.js +6 -6
  77. package/lib/helper/scripts/highlightElement.js +9 -9
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -1
  80. package/lib/history.js +23 -20
  81. package/lib/hooks.js +10 -10
  82. package/lib/html.js +90 -100
  83. package/lib/index.js +48 -21
  84. package/lib/listener/config.js +19 -12
  85. package/lib/listener/emptyRun.js +6 -7
  86. package/lib/listener/enhancedGlobalRetry.js +6 -6
  87. package/lib/listener/exit.js +4 -3
  88. package/lib/listener/globalRetry.js +5 -5
  89. package/lib/listener/globalTimeout.js +30 -14
  90. package/lib/listener/helpers.js +39 -14
  91. package/lib/listener/mocha.js +3 -4
  92. package/lib/listener/result.js +4 -5
  93. package/lib/listener/retryEnhancer.js +3 -3
  94. package/lib/listener/steps.js +8 -7
  95. package/lib/listener/store.js +3 -3
  96. package/lib/locator.js +213 -192
  97. package/lib/mocha/asyncWrapper.js +105 -62
  98. package/lib/mocha/bdd.js +99 -13
  99. package/lib/mocha/cli.js +59 -26
  100. package/lib/mocha/factory.js +78 -19
  101. package/lib/mocha/featureConfig.js +1 -1
  102. package/lib/mocha/gherkin.js +56 -24
  103. package/lib/mocha/hooks.js +12 -3
  104. package/lib/mocha/index.js +13 -4
  105. package/lib/mocha/inject.js +22 -5
  106. package/lib/mocha/scenarioConfig.js +2 -2
  107. package/lib/mocha/suite.js +9 -2
  108. package/lib/mocha/test.js +10 -7
  109. package/lib/mocha/ui.js +28 -18
  110. package/lib/output.js +10 -8
  111. package/lib/parser.js +44 -44
  112. package/lib/pause.js +15 -16
  113. package/lib/plugin/analyze.js +19 -12
  114. package/lib/plugin/auth.js +20 -21
  115. package/lib/plugin/autoDelay.js +12 -8
  116. package/lib/plugin/coverage.js +28 -11
  117. package/lib/plugin/customLocator.js +3 -3
  118. package/lib/plugin/customReporter.js +3 -2
  119. package/lib/plugin/enhancedRetryFailedStep.js +6 -6
  120. package/lib/plugin/heal.js +14 -9
  121. package/lib/plugin/htmlReporter.js +724 -99
  122. package/lib/plugin/pageInfo.js +10 -10
  123. package/lib/plugin/pauseOnFail.js +4 -3
  124. package/lib/plugin/retryFailedStep.js +48 -5
  125. package/lib/plugin/screenshotOnFail.js +75 -37
  126. package/lib/plugin/stepByStepReport.js +14 -14
  127. package/lib/plugin/stepTimeout.js +4 -3
  128. package/lib/plugin/subtitles.js +6 -5
  129. package/lib/recorder.js +33 -14
  130. package/lib/rerun.js +69 -26
  131. package/lib/result.js +4 -4
  132. package/lib/retryCoordinator.js +2 -2
  133. package/lib/secret.js +18 -17
  134. package/lib/session.js +95 -89
  135. package/lib/step/base.js +7 -7
  136. package/lib/step/comment.js +2 -2
  137. package/lib/step/config.js +1 -1
  138. package/lib/step/func.js +3 -3
  139. package/lib/step/helper.js +3 -3
  140. package/lib/step/meta.js +5 -5
  141. package/lib/step/record.js +11 -11
  142. package/lib/step/retry.js +3 -3
  143. package/lib/step/section.js +3 -3
  144. package/lib/step.js +7 -10
  145. package/lib/steps.js +9 -5
  146. package/lib/store.js +1 -1
  147. package/lib/template/heal.js +1 -1
  148. package/lib/template/prompts/generatePageObject.js +31 -0
  149. package/lib/template/prompts/healStep.js +13 -0
  150. package/lib/template/prompts/writeStep.js +9 -0
  151. package/lib/test-server.js +17 -6
  152. package/lib/timeout.js +1 -7
  153. package/lib/transform.js +8 -8
  154. package/lib/translation.js +32 -18
  155. package/lib/utils/mask_data.js +4 -10
  156. package/lib/utils.js +66 -64
  157. package/lib/workerStorage.js +17 -17
  158. package/lib/workers.js +214 -84
  159. package/package.json +41 -37
  160. package/translations/de-DE.js +2 -2
  161. package/translations/fr-FR.js +2 -2
  162. package/translations/index.js +23 -10
  163. package/translations/it-IT.js +2 -2
  164. package/translations/ja-JP.js +2 -2
  165. package/translations/nl-NL.js +2 -2
  166. package/translations/pl-PL.js +2 -2
  167. package/translations/pt-BR.js +2 -2
  168. package/translations/ru-RU.js +2 -2
  169. package/translations/utils.js +4 -3
  170. package/translations/zh-CN.js +2 -2
  171. package/translations/zh-TW.js +2 -2
  172. package/typings/index.d.ts +5 -3
  173. package/typings/promiseBasedTypes.d.ts +4 -0
  174. package/typings/types.d.ts +4 -0
  175. package/lib/helper/Nightmare.js +0 -1486
  176. package/lib/helper/Protractor.js +0 -1840
  177. package/lib/helper/TestCafe.js +0 -1391
  178. package/lib/helper/clientscripts/nightmare.js +0 -213
  179. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  180. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  181. package/lib/plugin/allure.js +0 -15
  182. package/lib/plugin/autoLogin.js +0 -5
  183. package/lib/plugin/commentStep.js +0 -141
  184. package/lib/plugin/eachElement.js +0 -127
  185. package/lib/plugin/fakerTransform.js +0 -49
  186. package/lib/plugin/retryTo.js +0 -16
  187. package/lib/plugin/selenoid.js +0 -364
  188. package/lib/plugin/standardActingHelpers.js +0 -6
  189. package/lib/plugin/tryTo.js +0 -16
  190. package/lib/plugin/wdio.js +0 -247
  191. package/lib/within.js +0 -90
@@ -1,4 +1,4 @@
1
- const tty = require('tty')
1
+ import tty from 'tty'
2
2
 
3
3
  if (!tty.getWindowSize) {
4
4
  // this is really old method, long removed from Node, but Mocha
@@ -7,33 +7,39 @@ if (!tty.getWindowSize) {
7
7
  tty.getWindowSize = () => [40, 80]
8
8
  }
9
9
 
10
- const { parentPort, workerData } = require('worker_threads')
11
- const event = require('../../event')
12
- const container = require('../../container')
13
- const { getConfig } = require('../utils')
14
- const { tryOrDefault, deepMerge } = require('../../utils')
10
+ import { parentPort, workerData } from 'worker_threads'
11
+
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
15
15
 
16
16
  let stdout = ''
17
17
 
18
18
  const stderr = ''
19
19
 
20
- // Requiring of Codecept need to be after tty.getWindowSize is available.
21
- const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')
22
-
23
20
  const { options, tests, testRoot, workerIndex, poolMode } = workerData
24
21
 
25
22
  // hide worker output
26
23
  // In pool mode, only suppress output if debug is NOT enabled
27
24
  // In regular mode, hide result output but allow step output in verbose/debug
28
25
  if (poolMode && !options.debug) {
29
- // In pool mode without debug, suppress only result summaries and failures, but allow Scenario Steps
26
+ // In pool mode without debug, allow test names and important output but suppress verbose details
30
27
  const originalWrite = process.stdout.write
31
28
  process.stdout.write = string => {
32
- // Always allow Scenario Steps output (including the circle symbol)
33
- if (string.includes('Scenario Steps:') || string.includes('◯ Scenario Steps:')) {
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
+ ) {
34
39
  return originalWrite.call(process.stdout, string)
35
40
  }
36
- if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:')) {
41
+ // Suppress result summaries to avoid duplicates
42
+ if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('◯ File:')) {
37
43
  return true
38
44
  }
39
45
  return originalWrite.call(process.stdout, string)
@@ -67,31 +73,62 @@ if (poolMode && !options.debug) {
67
73
  }
68
74
  }
69
75
 
70
- const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
71
-
72
- // important deep merge so dynamic things e.g. functions on config are not overridden
73
- 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
74
81
 
75
82
  // Load test and run
76
- const codecept = new Codecept(config, options)
77
- codecept.init(testRoot)
78
- codecept.loadTests()
79
- const mocha = container.mocha()
80
-
81
- if (poolMode) {
82
- // In pool mode, don't filter tests upfront - wait for assignments
83
- // We'll reload test files fresh for each test request
84
- } else {
85
- // Legacy mode - filter tests upfront
86
- filterTests()
87
- }
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
+ }
88
119
 
89
- // run tests
90
- ;(async function () {
91
- if (poolMode) {
92
- await runPoolTests()
93
- } else if (mocha.suite.total()) {
94
- await runTests()
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)
95
132
  }
96
133
  })()
97
134
 
@@ -123,6 +160,9 @@ async function runPoolTests() {
123
160
  initializeListeners()
124
161
  disablePause()
125
162
 
163
+ // Emit event.all.before once at the start of pool mode
164
+ event.dispatcher.emit(event.all.before, codecept)
165
+
126
166
  // Accumulate results across all tests in pool mode
127
167
  let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
128
168
  let allTests = []
@@ -131,25 +171,42 @@ async function runPoolTests() {
131
171
 
132
172
  // Keep requesting tests until no more available
133
173
  while (true) {
134
- // Request a test assignment
135
- sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
136
-
174
+ // Request a test assignment and wait for response
137
175
  const testResult = await new Promise((resolve, reject) => {
138
- // Set up pool mode message handler
176
+ // Set up pool mode message handler FIRST before sending request
139
177
  const messageHandler = async eventData => {
178
+ // Remove handler immediately to prevent duplicate processing
179
+ parentPort?.off('message', messageHandler)
180
+
140
181
  if (eventData.type === 'TEST_ASSIGNED') {
141
- const testUid = eventData.test
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
142
185
 
143
186
  try {
144
- // In pool mode, we need to create a fresh Mocha instance for each test
145
- // because Mocha instances become disposed after running tests
146
- container.createMocha() // Create fresh Mocha instance
147
- filterTestById(testUid)
187
+ // Create a fresh Mocha instance for each test file
188
+ container.createMocha()
148
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 */ }
149
198
 
150
199
  if (mocha.suite.total() > 0) {
151
- // Run the test and complete
152
- await codecept.run()
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
+ })
153
210
 
154
211
  // Get the results from this specific test run
155
212
  const result = container.result()
@@ -179,24 +236,26 @@ async function runPoolTests() {
179
236
  }
180
237
  }
181
238
 
182
- // Signal test completed and request next
183
- parentPort?.off('message', messageHandler)
239
+ // Signal test completed
184
240
  resolve('TEST_COMPLETED')
185
241
  } catch (err) {
186
- parentPort?.off('message', messageHandler)
187
242
  reject(err)
188
243
  }
189
244
  } else if (eventData.type === 'NO_MORE_TESTS') {
190
245
  // No tests available, exit worker
191
- parentPort?.off('message', messageHandler)
192
246
  resolve('NO_MORE_TESTS')
193
247
  } else {
194
248
  // Handle other message types (support messages, etc.)
195
249
  container.append({ support: eventData.data })
250
+ // Don't re-add handler - each test request creates its own one-time handler
196
251
  }
197
252
  }
198
253
 
254
+ // Set up handler BEFORE sending request to avoid race condition
199
255
  parentPort?.on('message', messageHandler)
256
+
257
+ // Now send the request
258
+ sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
200
259
  })
201
260
 
202
261
  // Exit if no more tests
@@ -205,6 +264,9 @@ async function runPoolTests() {
205
264
  }
206
265
  }
207
266
 
267
+ // Emit event.all.after once at the end of pool mode
268
+ event.dispatcher.emit(event.all.after, codecept)
269
+
208
270
  try {
209
271
  await codecept.teardown()
210
272
  } catch (err) {
@@ -232,70 +294,38 @@ async function runPoolTests() {
232
294
  }
233
295
 
234
296
  function filterTestById(testUid) {
235
- // Reload test files fresh for each test in pool mode
236
- const files = codecept.testFiles
237
-
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
+
238
300
  // Get the existing mocha instance
239
301
  const mocha = container.mocha()
240
302
 
303
+ // Save reference to all suites before clearing
304
+ const allSuites = [...mocha.suite.suites]
305
+
241
306
  // Clear suites and tests but preserve other mocha settings
242
307
  mocha.suite.suites = []
243
308
  mocha.suite.tests = []
244
309
 
245
- // Clear require cache for test files to ensure fresh loading
246
- files.forEach(file => {
247
- delete require.cache[require.resolve(file)]
248
- })
249
-
250
- // Set files and load them
251
- mocha.files = files
252
- mocha.loadFiles()
253
-
254
- // Now filter to only the target test - use a more robust approach
310
+ // Find and add only the suite containing our target test
255
311
  let foundTest = false
256
- for (const suite of mocha.suite.suites) {
312
+ for (const suite of allSuites) {
257
313
  const originalTests = [...suite.tests]
258
- suite.tests = []
259
-
260
- for (const test of originalTests) {
261
- if (test.uid === testUid) {
262
- suite.tests.push(test)
263
- foundTest = true
264
- break // Only add one matching test
265
- }
266
- }
267
-
268
- // If no tests found in this suite, remove it
269
- if (suite.tests.length === 0) {
270
- suite.parent.suites = suite.parent.suites.filter(s => s !== suite)
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
271
324
  }
272
325
  }
273
326
 
274
- // Filter out empty suites from the root
275
- mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
276
-
277
327
  if (!foundTest) {
278
- // If testUid doesn't match, maybe it's a simple test name - try fallback
279
- mocha.suite.suites = []
280
- mocha.suite.tests = []
281
- mocha.loadFiles()
282
-
283
- // Try matching by title
284
- for (const suite of mocha.suite.suites) {
285
- const originalTests = [...suite.tests]
286
- suite.tests = []
287
-
288
- for (const test of originalTests) {
289
- if (test.title === testUid || test.fullTitle() === testUid || test.uid === testUid) {
290
- suite.tests.push(test)
291
- foundTest = true
292
- break
293
- }
294
- }
295
- }
296
-
297
- // Clean up empty suites again
298
- mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
328
+ console.error(`WARNING: Test with UID ${testUid} not found in mocha suites`)
299
329
  }
300
330
  }
301
331
 
@@ -311,27 +341,36 @@ function filterTests() {
311
341
 
312
342
  function initializeListeners() {
313
343
  // suite
314
- event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: suite.simplify() }))
315
- event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: suite.simplify() }))
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() }))
316
346
 
317
347
  // calculate duration
318
348
  event.dispatcher.on(event.test.started, test => (test.start = new Date()))
319
349
 
320
350
  // tests
321
- event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: test.simplify() }))
322
- event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: test.simplify() }))
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() }))
323
353
  // we should force-send correct errors to prevent race condition
324
- event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: { ...test.simplify(), err } }))
325
- event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: { ...test.simplify(), err } }))
326
- event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } }))
327
- event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: test.simplify() }))
328
- event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: test.simplify() }))
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() }))
329
368
 
330
369
  // steps
331
- event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: step.simplify() }))
332
- event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: step.simplify() }))
333
- event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: step.simplify() }))
334
- event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: step.simplify() }))
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() }))
335
374
 
336
375
  event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } }))
337
376
  event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
@@ -357,6 +396,50 @@ function disablePause() {
357
396
  global.pause = () => {}
358
397
  }
359
398
 
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,
408
+ }
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
+ }
441
+ }
442
+
360
443
  function sendToParentThread(data) {
361
444
  parentPort?.postMessage(data)
362
445
  }