codeceptjs 4.0.2-beta.9 → 4.0.2

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 (326) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +84 -41
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. package/lib/retryCoordinator.js +0 -207
@@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'
11
11
 
12
12
  // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
13
13
  // These will be imported dynamically when needed
14
- let event, container, Codecept, getConfig, tryOrDefault, deepMerge
14
+ let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack
15
15
 
16
16
  let stdout = ''
17
17
 
@@ -21,25 +21,40 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData
21
21
 
22
22
  // Global error handlers to catch critical errors but not test failures
23
23
  process.on('uncaughtException', (err) => {
24
+ if (container?.tsFileMapping && fixErrorStack) {
25
+ const fileMapping = container.tsFileMapping()
26
+ if (fileMapping) {
27
+ fixErrorStack(err, fileMapping)
28
+ }
29
+ }
30
+
31
+ // Log to stderr to bypass stdout suppression
32
+ process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`)
33
+ process.stderr.write(`${err.stack}\n`)
34
+
24
35
  // Don't exit on test assertion errors - those are handled by mocha
25
36
  if (err.name === 'AssertionError' || err.message?.includes('expected')) {
26
- console.error(`[Worker ${workerIndex}] Test assertion error (handled by mocha):`, err.message)
27
37
  return
28
38
  }
29
- console.error(`[Worker ${workerIndex}] Uncaught exception:`, err.message)
30
- console.error(err.stack)
31
39
  process.exit(1)
32
40
  })
33
41
 
34
42
  process.on('unhandledRejection', (reason, promise) => {
35
- // Don't exit on test-related rejections
43
+ if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) {
44
+ const fileMapping = container.tsFileMapping()
45
+ if (fileMapping) {
46
+ fixErrorStack(reason, fileMapping)
47
+ }
48
+ }
49
+
50
+ // Log to stderr to bypass stdout suppression
36
51
  const msg = reason?.message || String(reason)
37
- if (msg.includes('expected') || msg.includes('AssertionError')) {
38
- console.error(`[Worker ${workerIndex}] Test rejection (handled by mocha):`, msg)
39
- return
52
+ process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`)
53
+ if (reason?.stack) {
54
+ process.stderr.write(`${reason.stack}\n`)
40
55
  }
41
- console.error(`[Worker ${workerIndex}] Unhandled rejection:`, reason)
42
- process.exit(1)
56
+
57
+ // Do not exit — killing the worker silently drops every remaining test from the report.
43
58
  })
44
59
 
45
60
  // hide worker output
@@ -49,6 +64,10 @@ if (poolMode && !options.debug) {
49
64
  // In pool mode without debug, allow test names and important output but suppress verbose details
50
65
  const originalWrite = process.stdout.write
51
66
  process.stdout.write = string => {
67
+ // Always allow Worker logs
68
+ if (string.includes('[Worker')) {
69
+ return originalWrite.call(process.stdout, string)
70
+ }
52
71
  // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
53
72
  if (
54
73
  string.includes('✔') ||
@@ -68,7 +87,12 @@ if (poolMode && !options.debug) {
68
87
  return originalWrite.call(process.stdout, string)
69
88
  }
70
89
  } else if (!poolMode && !options.debug && !options.verbose) {
90
+ const originalWrite = process.stdout.write
71
91
  process.stdout.write = string => {
92
+ // Always allow Worker logs
93
+ if (string.includes('[Worker')) {
94
+ return originalWrite.call(process.stdout, string)
95
+ }
72
96
  stdout += string
73
97
  return true
74
98
  }
@@ -105,24 +129,47 @@ let config
105
129
  // Load test and run
106
130
  initPromise = (async function () {
107
131
  try {
132
+ // Add staggered delay at the very start to prevent resource conflicts
133
+ // Longer delay for browser initialization conflicts
134
+ const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
135
+ if (delay > 0) {
136
+ await new Promise(resolve => setTimeout(resolve, delay))
137
+ }
138
+
108
139
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
109
140
  const eventModule = await import('../../event.js')
110
141
  const containerModule = await import('../../container.js')
111
142
  const utilsModule = await import('../utils.js')
112
143
  const coreUtilsModule = await import('../../utils.js')
113
144
  const CodeceptModule = await import('../../codecept.js')
114
-
145
+ const typescriptModule = await import('../../utils/typescript.js')
146
+
115
147
  event = eventModule.default
116
148
  container = containerModule.default
117
149
  getConfig = utilsModule.getConfig
118
150
  tryOrDefault = coreUtilsModule.tryOrDefault
119
151
  deepMerge = coreUtilsModule.deepMerge
120
152
  Codecept = CodeceptModule.default
153
+ fixErrorStack = typescriptModule.fixErrorStack
121
154
 
122
155
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
123
156
 
124
- // IMPORTANT: await is required here since getConfig is async
125
- const baseConfig = await getConfig(options.config || testRoot)
157
+ let baseConfig
158
+ try {
159
+ // IMPORTANT: await is required here since getConfig is async
160
+ baseConfig = await getConfig(options.config || testRoot)
161
+ } catch (configErr) {
162
+ if (container?.tsFileMapping && fixErrorStack) {
163
+ const fileMapping = container.tsFileMapping()
164
+ if (fileMapping) {
165
+ fixErrorStack(configErr, fileMapping)
166
+ }
167
+ }
168
+ process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`)
169
+ process.stderr.write(`${configErr.stack}\n`)
170
+ await new Promise(resolve => setTimeout(resolve, 100))
171
+ process.exit(1)
172
+ }
126
173
 
127
174
  // important deep merge so dynamic things e.g. functions on config are not overridden
128
175
  config = deepMerge(baseConfig, overrideConfigs)
@@ -130,7 +177,21 @@ initPromise = (async function () {
130
177
  // Pass workerIndex as child option for output.process() to display worker prefix
131
178
  const optsWithChild = { ...options, child: workerIndex }
132
179
  codecept = new Codecept(config, optsWithChild)
133
- await codecept.init(testRoot)
180
+
181
+ try {
182
+ await codecept.init(testRoot)
183
+ } catch (initErr) {
184
+ if (container?.tsFileMapping && fixErrorStack) {
185
+ const fileMapping = container.tsFileMapping()
186
+ if (fileMapping) {
187
+ fixErrorStack(initErr, fileMapping)
188
+ }
189
+ }
190
+ process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`)
191
+ process.stderr.write(`${initErr.stack}\n`)
192
+ process.exit(1)
193
+ }
194
+
134
195
  codecept.loadTests()
135
196
  mocha = container.mocha()
136
197
 
@@ -139,10 +200,7 @@ initPromise = (async function () {
139
200
  // We'll reload test files fresh for each test request
140
201
  } else {
141
202
  // Legacy mode - filter tests upfront
142
- console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
143
203
  filterTests()
144
- const finalCount = mocha.suite.total()
145
- console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
146
204
  }
147
205
 
148
206
  // run tests
@@ -156,7 +214,14 @@ initPromise = (async function () {
156
214
  parentPort?.close()
157
215
  }
158
216
  } catch (err) {
159
- console.error('Error in worker initialization:', err)
217
+ if (container?.tsFileMapping && fixErrorStack) {
218
+ const fileMapping = container.tsFileMapping()
219
+ if (fileMapping) {
220
+ fixErrorStack(err, fileMapping)
221
+ }
222
+ }
223
+ process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`)
224
+ process.stderr.write(`${err.stack}\n`)
160
225
  process.exit(1)
161
226
  }
162
227
  })()
@@ -167,7 +232,6 @@ async function runTests() {
167
232
  try {
168
233
  await codecept.bootstrap()
169
234
  } catch (err) {
170
- console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
171
235
  throw new Error(`Error while running bootstrap file :${err}`)
172
236
  }
173
237
  listenToParentThread()
@@ -176,13 +240,12 @@ async function runTests() {
176
240
  try {
177
241
  await codecept.run()
178
242
  } catch (err) {
179
- console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
180
243
  throw err
181
244
  } finally {
182
245
  try {
183
246
  await codecept.teardown()
184
247
  } catch (err) {
185
- console.error(`[Worker ${workerIndex}] Teardown error:`, err.message)
248
+ // Ignore teardown errors
186
249
  }
187
250
  }
188
251
  }
@@ -371,26 +434,6 @@ function filterTests() {
371
434
  mocha.files = files
372
435
  mocha.loadFiles()
373
436
 
374
- // Collect all loaded tests for debugging
375
- const allLoadedTests = [];
376
- mocha.suite.eachTest(test => {
377
- if (test) {
378
- allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
379
- }
380
- });
381
-
382
- console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests from ${files.length} files`);
383
- console.log(`[Worker ${workerIndex}] Expecting ${tests.length} test UIDs`);
384
-
385
- const loadedUids = new Set(allLoadedTests.map(t => t.uid));
386
- const missingTests = tests.filter(uid => !loadedUids.has(uid));
387
-
388
- if (missingTests.length > 0) {
389
- console.error(`[Worker ${workerIndex}] ERROR: ${missingTests.length} assigned tests NOT FOUND in loaded files!`);
390
- console.error(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
391
- console.error(`[Worker ${workerIndex}] Available UIDs:`, Array.from(loadedUids).slice(0, 5), '...');
392
- }
393
-
394
437
  // Recursively filter tests in all suites (including nested ones)
395
438
  const filterSuiteTests = (suite) => {
396
439
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
package/lib/config.js CHANGED
@@ -15,8 +15,9 @@ const defaultConfig = {
15
15
  hooks: [],
16
16
  gherkin: {},
17
17
  plugins: {
18
- screenshotOnFail: {
19
- enabled: true, // will be disabled by default in 2.0
18
+ screenshot: {
19
+ enabled: true,
20
+ on: 'fail',
20
21
  },
21
22
  },
22
23
  stepTimeout: 0,
@@ -32,9 +33,27 @@ const defaultConfig = {
32
33
  ],
33
34
  }
34
35
 
36
+ // Array<{ fn: (cfg) => void, ran: boolean, error?: Error }>
35
37
  let hooks = []
36
38
  let config = {}
37
39
 
40
+ // Apply a single hook against `cfg`, swallowing errors so one broken hook
41
+ // can't take down the whole run. The failure is logged through the
42
+ // framework's own output module (when available) so it shows up in test
43
+ // reports; the hook is still marked ran so it doesn't get retried.
44
+ function applyHook(hook, cfg) {
45
+ try {
46
+ hook.fn(cfg)
47
+ } catch (err) {
48
+ hook.error = err
49
+ const out = globalThis.codeceptjs?.output
50
+ if (out && typeof out.error === 'function') out.error(`config hook failed: ${err.message}`)
51
+ else console.error('config hook failed:', err)
52
+ } finally {
53
+ hook.ran = true
54
+ }
55
+ }
56
+
38
57
  const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
39
58
 
40
59
  /**
@@ -49,7 +68,11 @@ class Config {
49
68
  */
50
69
  static create(newConfig) {
51
70
  config = deepMerge(deepClone(defaultConfig), newConfig)
52
- hooks.forEach(f => f(config))
71
+ // Re-apply every hook against the freshly built config; hooks added later
72
+ // (e.g. from plugin boot) stay pending until runPendingHooks. Array
73
+ // iterators re-check length on each step, so hooks pushed during a hook
74
+ // execution are visited in this same pass.
75
+ for (const hook of hooks) applyHook(hook, config)
53
76
  return config
54
77
  }
55
78
 
@@ -121,7 +144,48 @@ class Config {
121
144
  }
122
145
 
123
146
  static addHook(fn) {
124
- hooks.push(fn)
147
+ hooks.push({ fn, ran: false })
148
+ }
149
+
150
+ /**
151
+ * Run every hook that hasn't been applied to the current config yet.
152
+ * Hooks added after `Config.create()` (e.g. from plugin boot code) stay
153
+ * pending until this is called; once it runs, they're marked applied so
154
+ * subsequent calls are no-ops. Hooks added while pending hooks are running
155
+ * are picked up in the same pass (the array iterator re-checks length).
156
+ *
157
+ * Failures are logged through `output.error` and don't abort the loop —
158
+ * a broken hook can't poison the run, but its error is visible.
159
+ *
160
+ * @param {Object<string, *>} [cfg] target config (defaults to the live singleton)
161
+ * @return {boolean} true if any hook ran
162
+ */
163
+ static runPendingHooks(cfg = config) {
164
+ let ran = false
165
+ for (const hook of hooks) {
166
+ if (hook.ran) continue
167
+ applyHook(hook, cfg)
168
+ ran = true
169
+ }
170
+ return ran
171
+ }
172
+
173
+ /**
174
+ * Number of registered config hooks. Useful for snapshotting before a phase
175
+ * (e.g. plugin loading) and re-running only the hooks added during it.
176
+ * @return {number}
177
+ */
178
+ static hooksCount() {
179
+ return hooks.length
180
+ }
181
+
182
+ /**
183
+ * Run hooks in `[fromIndex, end)` against the given config object, mutating it.
184
+ * @param {number} fromIndex
185
+ * @param {Object<string, *>} cfg
186
+ */
187
+ static runHooksFrom(fromIndex, cfg) {
188
+ for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
125
189
  }
126
190
 
127
191
  /**
@@ -150,33 +214,47 @@ async function loadConfigFile(configFile) {
150
214
  const require = createRequire(import.meta.url)
151
215
  const extensionName = path.extname(configFile)
152
216
 
217
+ // Populate the in-process registry that packages like @codeceptjs/configure
218
+ // look up at config-import time (their proxies throw if `globalThis.codeceptjs`
219
+ // is missing). initCodeceptGlobals sets this too, but only later during
220
+ // bootstrap — config files are imported here first.
221
+ if (!globalThis.codeceptjs) {
222
+ const indexModule = await import('./index.js')
223
+ globalThis.codeceptjs = indexModule.default || indexModule
224
+ }
225
+
153
226
  // .conf.js config file
154
227
  if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
155
228
  let configModule
156
229
  try {
157
230
  // For .ts files, try to compile and load as JavaScript
158
231
  if (extensionName === '.ts') {
232
+ let transpileError = null
233
+ let tempFile = null
234
+ let allTempFiles = null
235
+ let fileMapping = null
236
+
159
237
  try {
160
238
  // Use the TypeScript transpilation utility
161
239
  const typescript = require('typescript')
162
- const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript)
240
+ const result = await transpileTypeScript(configFile, typescript)
241
+ tempFile = result.tempFile
242
+ allTempFiles = result.allTempFiles
243
+ fileMapping = result.fileMapping
163
244
 
164
- try {
165
- configModule = await import(tempFile)
166
- cleanupTempFiles(allTempFiles)
167
- } catch (err) {
245
+ configModule = await import(tempFile)
246
+ cleanupTempFiles(allTempFiles)
247
+ } catch (err) {
248
+ transpileError = err
249
+ if (fileMapping) {
168
250
  fixErrorStack(err, fileMapping)
169
- cleanupTempFiles(allTempFiles)
170
- throw err
171
251
  }
172
- } catch (tsError) {
173
- // If TypeScript compilation fails, fallback to ts-node
174
- try {
175
- require('ts-node/register')
176
- configModule = require(configFile)
177
- } catch (tsNodeError) {
178
- throw new Error(`Failed to load TypeScript config: ${tsError.message}`)
252
+ if (allTempFiles) {
253
+ cleanupTempFiles(allTempFiles)
179
254
  }
255
+ // Throw immediately with the actual error - don't fall back to ts-node
256
+ // as it will mask the real error with "Unexpected token 'export'"
257
+ throw err
180
258
  }
181
259
  } else {
182
260
  // Try ESM import first for JS files
package/lib/container.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { globSync } from 'glob'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
+ import { isMainThread } from 'worker_threads'
4
5
  import debugModule from 'debug'
5
6
  const debug = debugModule('codeceptjs:container')
6
7
  import { MetaStep } from './step.js'
@@ -15,9 +16,15 @@ import store from './store.js'
15
16
  import Result from './result.js'
16
17
  import ai from './ai.js'
17
18
  import actorFactory from './actor.js'
19
+ import Config from './config.js'
18
20
 
19
21
  let asyncHelperPromise
20
22
 
23
+ let beforeCalledSet = new Set()
24
+
25
+ export function getBeforeCalledSet() { return beforeCalledSet }
26
+ export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
27
+
21
28
  let container = {
22
29
  helpers: {},
23
30
  support: {},
@@ -116,6 +123,18 @@ class Container {
116
123
  // Wait for all async helpers to finish loading and populate the actor
117
124
  await asyncHelperPromise
118
125
 
126
+ // Plugins may have registered Config hooks during their boot. Run anything
127
+ // that hasn't been applied yet and re-feed the mutated helper config to the
128
+ // already-instantiated helpers.
129
+ if (Config.runPendingHooks(config)) {
130
+ for (const name of Object.keys(container.helpers)) {
131
+ const helper = container.helpers[name]
132
+ if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
133
+ helper._setConfig(config.helpers[name])
134
+ }
135
+ }
136
+ }
137
+
119
138
  if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
120
139
  if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
121
140
  if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
@@ -150,10 +169,23 @@ class Container {
150
169
  if (!name) {
151
170
  return container.proxySupport
152
171
  }
153
- // Always return the proxy to ensure MetaStep creation works
172
+ if (typeof container.support[name] === 'function') {
173
+ return container.support[name]
174
+ }
154
175
  return container.proxySupport[name]
155
176
  }
156
177
 
178
+ /**
179
+ * Get raw (non-proxied) support objects for direct access.
180
+ * Used by listeners to call lifecycle hooks without MetaStep wrapping.
181
+ *
182
+ * @api
183
+ * @returns {object}
184
+ */
185
+ static supportObjects() {
186
+ return container.support
187
+ }
188
+
157
189
  /**
158
190
  * Get all helpers or get a helper by name
159
191
  *
@@ -183,7 +215,7 @@ class Container {
183
215
  * @api
184
216
  */
185
217
  static tsFileMapping() {
186
- return container.tsFileMapping
218
+ return store.tsFileMapping
187
219
  }
188
220
 
189
221
  /**
@@ -426,11 +458,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
426
458
  tempJsFile = allTempFiles
427
459
  fileMapping = mapping
428
460
  // Store file mapping in container for runtime error fixing (merge with existing)
429
- if (!container.tsFileMapping) {
430
- container.tsFileMapping = new Map()
461
+ if (!store.tsFileMapping) {
462
+ store.tsFileMapping = new Map()
431
463
  }
432
464
  for (const [key, value] of mapping.entries()) {
433
- container.tsFileMapping.set(key, value)
465
+ store.tsFileMapping.set(key, value)
434
466
  }
435
467
  } catch (tsError) {
436
468
  throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
@@ -542,6 +574,19 @@ function createSupportObjects(config) {
542
574
  let currentValue = currentObject[prop]
543
575
 
544
576
  if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
577
+ if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
578
+ beforeCalledSet.add(name)
579
+ const originalValue = currentValue
580
+ const wrappedValue = async function (...args) {
581
+ await currentObject._before()
582
+ return originalValue.apply(currentObject, args)
583
+ }
584
+ const ms = new MetaStep(name, prop)
585
+ ms.setContext(currentObject)
586
+ debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
587
+ return ms.run.bind(ms, asyncWrapper(wrappedValue))
588
+ }
589
+
545
590
  const ms = new MetaStep(name, prop)
546
591
  ms.setContext(currentObject)
547
592
  if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
@@ -600,6 +645,8 @@ function createSupportObjects(config) {
600
645
  let value
601
646
  if (container.sharedKeys.has(prop) && prop in container.support) {
602
647
  value = container.support[prop]
648
+ } else if (prop in container.support && typeof container.support[prop] === 'function') {
649
+ value = container.support[prop]
603
650
  } else {
604
651
  value = lazyLoad(prop)
605
652
  }
@@ -614,6 +661,9 @@ function createSupportObjects(config) {
614
661
  if (container.sharedKeys.has(key) && key in container.support) {
615
662
  return container.support[key]
616
663
  }
664
+ if (key in container.support && typeof container.support[key] === 'function') {
665
+ return container.support[key]
666
+ }
617
667
  return lazyLoad(key)
618
668
  },
619
669
  },
@@ -654,26 +704,55 @@ async function loadPluginFallback(modulePath, config) {
654
704
  async function createPlugins(config, options = {}) {
655
705
  const plugins = {}
656
706
 
657
- const enabledPluginsByOptions = (options.plugins || '').split(',')
707
+ const pluginOptionMap = new Map()
708
+ for (const token of (options.plugins || '').split(',').filter(Boolean)) {
709
+ const parts = token.split(':')
710
+ pluginOptionMap.set(parts[0], parts.slice(1))
711
+ }
712
+
713
+ for (const [name] of pluginOptionMap) {
714
+ if (!config[name]) config[name] = {}
715
+ }
716
+
658
717
  for (const pluginName in config) {
659
718
  if (!config[pluginName]) config[pluginName] = {}
660
- if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
719
+ const pluginConfig = config[pluginName]
720
+ const enabledByCli = pluginOptionMap.has(pluginName)
721
+ if (!pluginConfig.enabled && !enabledByCli) {
661
722
  continue // plugin is disabled
662
723
  }
724
+
725
+ if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
726
+ pluginConfig._args = pluginOptionMap.get(pluginName)
727
+ }
728
+
729
+ // Generic workers gate:
730
+ // - runInWorker / runInWorkers controls plugin execution inside worker threads.
731
+ // - runInParent / runInMain can disable plugin in workers parent process.
732
+ const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
733
+ const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
734
+
735
+ if (!isMainThread && !runInWorker) {
736
+ continue
737
+ }
738
+
739
+ if (isMainThread && store.workerMode && !runInParent) {
740
+ continue
741
+ }
663
742
  let module
664
743
  try {
665
- if (config[pluginName].require) {
666
- module = config[pluginName].require
744
+ if (pluginConfig.require) {
745
+ module = pluginConfig.require
667
746
  if (module.startsWith('.')) {
668
747
  // local
669
- module = path.resolve(global.codecept_dir, module) // custom plugin
748
+ module = path.resolve(store.codeceptDir, module) // custom plugin
670
749
  }
671
750
  } else {
672
751
  module = `./plugin/${pluginName}.js`
673
752
  }
674
753
 
675
754
  // Use async loading for all plugins (ESM and CJS)
676
- plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
755
+ plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
677
756
  debug(`plugin ${pluginName} loaded via async import`)
678
757
  } catch (err) {
679
758
  throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
@@ -683,12 +762,24 @@ async function createPlugins(config, options = {}) {
683
762
  }
684
763
 
685
764
  async function loadGherkinStepsAsync(paths) {
765
+ // Import BDD module to access step file tracking functions and step DSL
766
+ const bddModule = await import('./mocha/bdd.js')
767
+
686
768
  global.Before = fn => event.dispatcher.on(event.test.started, fn)
687
769
  global.After = fn => event.dispatcher.on(event.test.finished, fn)
688
770
  global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
689
771
 
690
- // Import BDD module to access step file tracking functions
691
- const bddModule = await import('./mocha/bdd.js')
772
+ // Scope-inject Given/When/Then/And while loading step files so they work
773
+ // with noGlobals: true. When noGlobals: false, globals.js has already set
774
+ // them as permanent globals — skip to avoid deleting them at the end.
775
+ const injectStepDsl = !!store.noGlobals
776
+ if (injectStepDsl) {
777
+ global.Given = bddModule.Given
778
+ global.When = bddModule.When
779
+ global.Then = bddModule.Then
780
+ global.And = bddModule.And
781
+ global.DefineParameterType = bddModule.defineParameterType
782
+ }
692
783
 
693
784
  // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
694
785
  // If gherkin.steps is Array, it will go the old way
@@ -701,7 +792,7 @@ async function loadGherkinStepsAsync(paths) {
701
792
  bddModule.clearCurrentStepFile()
702
793
  }
703
794
  } else {
704
- const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
795
+ const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
705
796
  if (folderPath !== '') {
706
797
  const files = globSync(folderPath)
707
798
  for (const file of files) {
@@ -716,6 +807,13 @@ async function loadGherkinStepsAsync(paths) {
716
807
  delete global.Before
717
808
  delete global.After
718
809
  delete global.Fail
810
+ if (injectStepDsl) {
811
+ delete global.Given
812
+ delete global.When
813
+ delete global.Then
814
+ delete global.And
815
+ delete global.DefineParameterType
816
+ }
719
817
  }
720
818
 
721
819
  function loadGherkinSteps(paths) {
@@ -749,7 +847,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
749
847
  }
750
848
  }
751
849
  if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
752
- modulePath = path.join(global.codecept_dir, modulePath)
850
+ modulePath = path.join(store.codeceptDir, modulePath)
753
851
  }
754
852
  try {
755
853
  // Use dynamic import for both ESM and CJS modules
@@ -873,7 +971,7 @@ async function loadTranslation(locale, vocabularies) {
873
971
  const langs = await Translation.getLangs()
874
972
  if (langs[locale]) {
875
973
  translation = new Translation(langs[locale])
876
- } else if (fileExists(path.join(global.codecept_dir, locale))) {
974
+ } else if (fileExists(path.join(store.codeceptDir, locale))) {
877
975
  // get from a provided file instead
878
976
  translation = Translation.createDefault()
879
977
  translation.loadVocabulary(locale)
@@ -890,7 +988,7 @@ function getHelperModuleName(helperName, config) {
890
988
  // classical require
891
989
  if (config[helperName].require) {
892
990
  if (config[helperName].require.startsWith('.')) {
893
- let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
991
+ let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
894
992
  // Add .js extension if not present for ESM compatibility
895
993
  if (!path.extname(helperPath)) {
896
994
  helperPath += '.js'