codeceptjs 4.0.2-beta.8 → 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 +91 -37
  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
@@ -1,5 +1,60 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
+ import { pathToFileURL } from 'url'
4
+
5
+ /**
6
+ * Load tsconfig.json if it exists
7
+ * @param {string} tsConfigPath - Path to tsconfig.json
8
+ * @returns {object|null} - Parsed tsconfig or null
9
+ */
10
+ function loadTsConfig(tsConfigPath) {
11
+ if (!fs.existsSync(tsConfigPath)) {
12
+ return null
13
+ }
14
+
15
+ try {
16
+ const tsConfigContent = fs.readFileSync(tsConfigPath, 'utf8')
17
+ return JSON.parse(tsConfigContent)
18
+ } catch (err) {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Resolve TypeScript path alias to actual file path
25
+ * @param {string} importPath - Import path with alias (e.g., '#config/urls')
26
+ * @param {object} tsConfig - Parsed tsconfig.json
27
+ * @param {string} configDir - Directory containing tsconfig.json
28
+ * @returns {string|null} - Resolved file path or null if not an alias
29
+ */
30
+ function resolveTsPathAlias(importPath, tsConfig, configDir) {
31
+ if (!tsConfig || !tsConfig.compilerOptions || !tsConfig.compilerOptions.paths) {
32
+ return null
33
+ }
34
+
35
+ const paths = tsConfig.compilerOptions.paths
36
+
37
+ for (const [pattern, targets] of Object.entries(paths)) {
38
+ if (!targets || targets.length === 0) {
39
+ continue
40
+ }
41
+
42
+ const patternRegex = new RegExp(
43
+ '^' + pattern.replace(/\*/g, '(.*)') + '$'
44
+ )
45
+ const match = importPath.match(patternRegex)
46
+
47
+ if (match) {
48
+ const wildcard = match[1] || ''
49
+ const target = targets[0]
50
+ const resolvedTarget = target.replace(/\*/g, wildcard)
51
+
52
+ return path.resolve(configDir, resolvedTarget)
53
+ }
54
+ }
55
+
56
+ return null
57
+ }
3
58
 
4
59
  /**
5
60
  * Transpile TypeScript files to ES modules with CommonJS shim support
@@ -108,6 +163,22 @@ const __dirname = __dirname_fn(__filename);
108
163
  const transpiledFiles = new Map()
109
164
  const baseDir = path.dirname(mainFilePath)
110
165
 
166
+ // Try to find tsconfig.json by walking up the directory tree
167
+ let tsConfigPath = path.join(baseDir, 'tsconfig.json')
168
+ let configDir = baseDir
169
+ let searchDir = baseDir
170
+
171
+ while (!fs.existsSync(tsConfigPath) && searchDir !== path.dirname(searchDir)) {
172
+ searchDir = path.dirname(searchDir)
173
+ tsConfigPath = path.join(searchDir, 'tsconfig.json')
174
+ if (fs.existsSync(tsConfigPath)) {
175
+ configDir = searchDir
176
+ break
177
+ }
178
+ }
179
+
180
+ const tsConfig = loadTsConfig(tsConfigPath)
181
+
111
182
  // Recursive function to transpile a file and all its TypeScript dependencies
112
183
  const transpileFileAndDeps = (filePath) => {
113
184
  // Already transpiled, skip
@@ -118,21 +189,36 @@ const __dirname = __dirname_fn(__filename);
118
189
  // Transpile this file
119
190
  let jsContent = transpileTS(filePath)
120
191
 
121
- // Find all relative TypeScript imports in this file
122
- const importRegex = /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g
192
+ // Find all TypeScript imports in this file (both ESM imports and require() calls)
193
+ const importRegex = /from\s+['"]([^'"]+?)['"]/g
194
+ const requireRegex = /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g
123
195
  let match
124
196
  const imports = []
125
197
 
126
198
  while ((match = importRegex.exec(jsContent)) !== null) {
127
- imports.push(match[1])
199
+ imports.push({ path: match[1], type: 'import' })
200
+ }
201
+
202
+ while ((match = requireRegex.exec(jsContent)) !== null) {
203
+ imports.push({ path: match[1], type: 'require' })
128
204
  }
129
205
 
130
206
  // Get the base directory for this file
131
207
  const fileBaseDir = path.dirname(filePath)
132
208
 
133
209
  // Recursively transpile each imported TypeScript file
134
- for (const relativeImport of imports) {
135
- let importedPath = path.resolve(fileBaseDir, relativeImport)
210
+ for (const { path: importPath } of imports) {
211
+ let importedPath = importPath
212
+
213
+ // Check if this is a path alias
214
+ const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
215
+ if (resolvedAlias) {
216
+ importedPath = resolvedAlias
217
+ } else if (importPath.startsWith('.')) {
218
+ importedPath = path.resolve(fileBaseDir, importPath)
219
+ } else {
220
+ continue
221
+ }
136
222
 
137
223
  // Handle .js extensions that might actually be .ts files
138
224
  if (importedPath.endsWith('.js')) {
@@ -153,11 +239,17 @@ const __dirname = __dirname_fn(__filename);
153
239
  if (fs.existsSync(tsPath)) {
154
240
  importedPath = tsPath
155
241
  } else {
156
- // Try .js extension as well
157
- const jsPath = importedPath + '.js'
158
- if (fs.existsSync(jsPath)) {
159
- // Skip .js files, they don't need transpilation
160
- continue
242
+ // Try index.ts for directory imports
243
+ const indexTsPath = path.join(importedPath, 'index.ts')
244
+ if (fs.existsSync(indexTsPath)) {
245
+ importedPath = indexTsPath
246
+ } else {
247
+ // Try .js extension as well
248
+ const jsPath = importedPath + '.js'
249
+ if (fs.existsSync(jsPath)) {
250
+ // Skip .js files, they don't need transpilation
251
+ continue
252
+ }
161
253
  }
162
254
  }
163
255
  }
@@ -170,24 +262,45 @@ const __dirname = __dirname_fn(__filename);
170
262
 
171
263
  // After all dependencies are transpiled, rewrite imports in this file
172
264
  jsContent = jsContent.replace(
173
- /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g,
265
+ /from\s+['"]([^'"]+?)['"]/g,
174
266
  (match, importPath) => {
175
- let resolvedPath = path.resolve(fileBaseDir, importPath)
267
+ let resolvedPath = importPath
176
268
  const originalExt = path.extname(importPath)
177
269
 
270
+ // Check if this is a path alias
271
+ const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
272
+ if (resolvedAlias) {
273
+ resolvedPath = resolvedAlias
274
+ } else if (importPath.startsWith('.')) {
275
+ resolvedPath = path.resolve(fileBaseDir, importPath)
276
+ } else {
277
+ return match
278
+ }
279
+
280
+ // If resolved path is a directory, try index.ts
281
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
282
+ const indexPath = path.join(resolvedPath, 'index.ts')
283
+ if (fs.existsSync(indexPath) && transpiledFiles.has(indexPath)) {
284
+ const tempFile = transpiledFiles.get(indexPath)
285
+ const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
286
+ if (!relPath.startsWith('.')) {
287
+ return `from './${relPath}'`
288
+ }
289
+ return `from '${relPath}'`
290
+ }
291
+ }
292
+
178
293
  // Handle .js extension that might be .ts
179
294
  if (resolvedPath.endsWith('.js')) {
180
295
  const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
181
296
  if (transpiledFiles.has(tsVersion)) {
182
297
  const tempFile = transpiledFiles.get(tsVersion)
183
298
  const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
184
- // Ensure the path starts with ./
185
299
  if (!relPath.startsWith('.')) {
186
300
  return `from './${relPath}'`
187
301
  }
188
302
  return `from '${relPath}'`
189
303
  }
190
- // Keep .js extension as-is (might be a real .js file)
191
304
  return match
192
305
  }
193
306
 
@@ -198,18 +311,24 @@ const __dirname = __dirname_fn(__filename);
198
311
  if (transpiledFiles.has(tsPath)) {
199
312
  const tempFile = transpiledFiles.get(tsPath)
200
313
  const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
201
- // Ensure the path starts with ./
202
314
  if (!relPath.startsWith('.')) {
203
315
  return `from './${relPath}'`
204
316
  }
205
317
  return `from '${relPath}'`
206
318
  }
207
319
 
208
- // If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json)
209
- // add .js for ESM compatibility
210
- // This handles cases where:
211
- // 1. Import has no real extension (e.g., "./utils" or "./helper")
212
- // 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper")
320
+ // Try index.ts for directory imports
321
+ const indexTsPath = path.join(resolvedPath, 'index.ts')
322
+ if (transpiledFiles.has(indexTsPath)) {
323
+ const tempFile = transpiledFiles.get(indexTsPath)
324
+ const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
325
+ if (!relPath.startsWith('.')) {
326
+ return `from './${relPath}'`
327
+ }
328
+ return `from '${relPath}'`
329
+ }
330
+
331
+ // If the import doesn't have a standard module extension, add .js for ESM compatibility
213
332
  const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
214
333
  const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
215
334
 
@@ -217,7 +336,50 @@ const __dirname = __dirname_fn(__filename);
217
336
  return match.replace(importPath, importPath + '.js')
218
337
  }
219
338
 
220
- // Otherwise, keep the import as-is
339
+ return match
340
+ }
341
+ )
342
+
343
+ // Also rewrite require() calls to point to transpiled TypeScript files
344
+ jsContent = jsContent.replace(
345
+ /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g,
346
+ (match, requirePath) => {
347
+ let resolvedPath = requirePath
348
+
349
+ // Check if this is a path alias
350
+ const resolvedAlias = resolveTsPathAlias(requirePath, tsConfig, configDir)
351
+ if (resolvedAlias) {
352
+ resolvedPath = resolvedAlias
353
+ } else if (requirePath.startsWith('.')) {
354
+ resolvedPath = path.resolve(fileBaseDir, requirePath)
355
+ } else {
356
+ return match
357
+ }
358
+
359
+ // Handle .js extension that might be .ts
360
+ if (resolvedPath.endsWith('.js')) {
361
+ const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
362
+ if (transpiledFiles.has(tsVersion)) {
363
+ const tempFile = transpiledFiles.get(tsVersion)
364
+ const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
365
+ const finalPath = relPath.startsWith('.') ? relPath : './' + relPath
366
+ return `require('${finalPath}')`
367
+ }
368
+ return match
369
+ }
370
+
371
+ // Try with .ts extension
372
+ const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
373
+
374
+ // If we transpiled this file, use the temp file
375
+ if (transpiledFiles.has(tsPath)) {
376
+ const tempFile = transpiledFiles.get(tsPath)
377
+ const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
378
+ const finalPath = relPath.startsWith('.') ? relPath : './' + relPath
379
+ return `require('${finalPath}')`
380
+ }
381
+
382
+ // Otherwise, keep the require as-is
221
383
  return match
222
384
  }
223
385
  )
@@ -234,10 +396,13 @@ const __dirname = __dirname_fn(__filename);
234
396
  // Get the main transpiled file
235
397
  const tempJsFile = transpiledFiles.get(mainFilePath)
236
398
 
237
- // Store all temp files for cleanup
399
+ // Convert to file:// URL for dynamic import() (required on Windows)
400
+ const tempFileUrl = pathToFileURL(tempJsFile).href
401
+
402
+ // Store all temp files for cleanup (keep as paths, not URLs)
238
403
  const allTempFiles = Array.from(transpiledFiles.values())
239
404
 
240
- return { tempFile: tempJsFile, allTempFiles, fileMapping: transpiledFiles }
405
+ return { tempFile: tempFileUrl, allTempFiles, fileMapping: transpiledFiles }
241
406
  }
242
407
 
243
408
  /**
package/lib/utils.js CHANGED
@@ -7,6 +7,7 @@ import getFunctionArguments from 'fn-args'
7
7
  import deepClone from 'lodash.clonedeep'
8
8
  import merge from 'lodash.merge'
9
9
  import { convertColorToRGBA, isColorProperty } from './colorUtils.js'
10
+ import store from './store.js'
10
11
  import Fuse from 'fuse.js'
11
12
  import crypto from 'crypto'
12
13
  import jsBeautify from 'js-beautify'
@@ -150,6 +151,24 @@ export const decodeUrl = function (url) {
150
151
  return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)))
151
152
  }
152
153
 
154
+ export const normalizePath = function (path) {
155
+ if (path === '' || path === '/') return '/'
156
+ return path
157
+ .replace(/\/+/g, '/')
158
+ .replace(/\/$/, '') || '/'
159
+ }
160
+
161
+ export const resolveUrl = function (url, baseUrl) {
162
+ if (!url) return url
163
+ if (url.indexOf('http') === 0) return url
164
+ if (!baseUrl) return url
165
+ try {
166
+ return new URL(url, baseUrl).href
167
+ } catch (e) {
168
+ return url
169
+ }
170
+ }
171
+
153
172
  export const xpathLocator = {
154
173
  /**
155
174
  * @param {string} string
@@ -317,13 +336,13 @@ export const screenshotOutputFolder = function (fileName) {
317
336
  const fileSep = path.sep
318
337
 
319
338
  if (!fileName.includes(fileSep) || fileName.includes('record_')) {
320
- return path.resolve(global.output_dir, fileName)
339
+ return path.resolve(store.outputDir, fileName)
321
340
  }
322
- return path.resolve(global.codecept_dir, fileName)
341
+ return path.resolve(store.codeceptDir, fileName)
323
342
  }
324
343
 
325
344
  export const relativeDir = function (fileName) {
326
- return fileName.replace(global.codecept_dir, '').replace(/^\//, '')
345
+ return fileName.replace(store.codeceptDir, '').replace(/^\//, '')
327
346
  }
328
347
 
329
348
  export const beautify = function (code) {
@@ -598,6 +617,12 @@ function createCircularSafeReplacer(keysToSkip = []) {
598
617
  return undefined
599
618
  }
600
619
 
620
+ // Coerce types that JSON.stringify can't handle natively
621
+ if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
622
+ if (typeof value === 'bigint') return `${value.toString()}n`
623
+ if (typeof value === 'symbol') return value.toString()
624
+ if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack }
625
+
601
626
  if (value === null || typeof value !== 'object') {
602
627
  return value
603
628
  }
@@ -628,6 +653,25 @@ export const safeStringify = function (obj, keysToSkip = [], space = 0) {
628
653
  }
629
654
  }
630
655
 
656
+ /**
657
+ * Truncate a string at a byte cap, returning structured info.
658
+ * @param {string} str
659
+ * @param {number} maxBytes
660
+ * @returns {{ value: string, truncated: boolean, fullLength: number }}
661
+ */
662
+ export const truncateString = function (str, maxBytes) {
663
+ if (typeof str !== 'string') str = String(str)
664
+ if (str.length <= maxBytes) {
665
+ return { value: str, truncated: false, fullLength: str.length }
666
+ }
667
+ const dropped = str.length - maxBytes
668
+ return {
669
+ value: `${str.slice(0, maxBytes)}\n...[truncated ${dropped} more chars]`,
670
+ truncated: true,
671
+ fullLength: str.length,
672
+ }
673
+ }
674
+
631
675
  export const serializeError = function (error) {
632
676
  if (error) {
633
677
  const { stack, uncaught, message, actual, expected } = error
@@ -640,6 +684,36 @@ export const base64EncodeFile = function (filePath) {
640
684
  return Buffer.from(fs.readFileSync(filePath)).toString('base64')
641
685
  }
642
686
 
687
+ export const getMimeType = function (fileName) {
688
+ const ext = path.extname(fileName).toLowerCase()
689
+ const mimeTypes = {
690
+ '.jpg': 'image/jpeg',
691
+ '.jpeg': 'image/jpeg',
692
+ '.png': 'image/png',
693
+ '.gif': 'image/gif',
694
+ '.bmp': 'image/bmp',
695
+ '.svg': 'image/svg+xml',
696
+ '.webp': 'image/webp',
697
+ '.pdf': 'application/pdf',
698
+ '.txt': 'text/plain',
699
+ '.html': 'text/html',
700
+ '.css': 'text/css',
701
+ '.js': 'application/javascript',
702
+ '.json': 'application/json',
703
+ '.xml': 'application/xml',
704
+ '.zip': 'application/zip',
705
+ '.csv': 'text/csv',
706
+ '.doc': 'application/msword',
707
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
708
+ '.xls': 'application/vnd.ms-excel',
709
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
710
+ '.mp3': 'audio/mpeg',
711
+ '.mp4': 'video/mp4',
712
+ '.wav': 'audio/wav',
713
+ }
714
+ return mimeTypes[ext] || 'application/octet-stream'
715
+ }
716
+
643
717
  export const markdownToAnsi = function (markdown) {
644
718
  return (
645
719
  markdown
package/lib/workers.js CHANGED
@@ -20,6 +20,7 @@ import event from './event.js'
20
20
  import { deserializeTest } from './mocha/test.js'
21
21
  import { deserializeSuite } from './mocha/suite.js'
22
22
  import recorder from './recorder.js'
23
+ import store from './store.js'
23
24
  import runHook from './hooks.js'
24
25
  import WorkerStorage from './workerStorage.js'
25
26
  import { createRuns } from './command/run-multiple/collection.js'
@@ -28,7 +29,7 @@ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
28
29
 
29
30
  const initializeCodecept = async (configPath, options = {}) => {
30
31
  const config = await mainConfig.load(configPath || '.')
31
- const codecept = new Codecept(config, options)
32
+ const codecept = new Codecept(config, { ...options, skipDefaultListeners: true })
32
33
  await codecept.init(getTestRoot(configPath))
33
34
  codecept.loadTests()
34
35
 
@@ -117,11 +118,9 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns
117
118
  const workersToExecute = []
118
119
 
119
120
  const currentOutputFolder = config.output
120
- let currentMochawesomeReportDir
121
121
  let currentMochaJunitReporterFile
122
122
 
123
123
  if (config.mocha && config.mocha.reporterOptions) {
124
- currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir
125
124
  currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile
126
125
  }
127
126
 
@@ -131,8 +130,6 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns
131
130
  let workerName = worker.name.replace(':', '_')
132
131
  _config.output = `${currentOutputFolder}${separator}${workerName}`
133
132
  if (config.mocha && config.mocha.reporterOptions) {
134
- _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`
135
-
136
133
  const _tempArray = currentMochaJunitReporterFile.split(separator)
137
134
  _tempArray.splice(
138
135
  _tempArray.findIndex(item => item.includes('.xml')),
@@ -224,7 +221,8 @@ class WorkerObject {
224
221
  const oldConfig = JSON.parse(this.options.override || '{}')
225
222
 
226
223
  // Remove customLocatorStrategies from both old and new config before JSON serialization
227
- // since functions cannot be serialized and will be lost, causing workers to have empty strategies
224
+ // since functions cannot be serialized and will be lost, causing workers to have empty strategies.
225
+ // Note: Only WebDriver helper supports customLocatorStrategies
228
226
  const configWithoutFunctions = { ...config }
229
227
 
230
228
  // Clean both old and new config
@@ -503,6 +501,7 @@ class Workers extends EventEmitter {
503
501
  await this._ensureInitialized()
504
502
  recorder.startUnlessRunning()
505
503
  event.dispatcher.emit(event.workers.before)
504
+ store.workerMode = true
506
505
  process.env.RUNS_WITH_WORKERS = 'true'
507
506
 
508
507
  // Create workers and set up message handlers immediately (not in recorder queue)
@@ -518,22 +517,8 @@ class Workers extends EventEmitter {
518
517
  // Workers are already running, this is just a placeholder step
519
518
  })
520
519
 
521
- // Add overall timeout to prevent infinite hanging
522
- const overallTimeout = setTimeout(() => {
523
- console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
524
- workerThreads.forEach(w => {
525
- try {
526
- w.terminate()
527
- } catch (e) {
528
- // ignore
529
- }
530
- })
531
- this._finishRun()
532
- }, 600000) // 10 minutes
533
-
534
520
  return new Promise(resolve => {
535
521
  this.on('end', () => {
536
- clearTimeout(overallTimeout)
537
522
  resolve()
538
523
  })
539
524
  })
@@ -558,11 +543,12 @@ class Workers extends EventEmitter {
558
543
  if (this.isPoolMode) {
559
544
  this.activeWorkers.set(worker, { available: true, workerIndex: null })
560
545
  }
561
-
546
+
562
547
  // Track last activity time to detect hanging workers
563
548
  let lastActivity = Date.now()
564
549
  let currentTest = null
565
- const workerTimeout = 300000 // 5 minutes
550
+ let autoTerminated = false
551
+ const workerTimeout = process.env.CODECEPT_WORKER_TIMEOUT ? ms(process.env.CODECEPT_WORKER_TIMEOUT) : ms('5m')
566
552
 
567
553
  const timeoutChecker = setInterval(() => {
568
554
  const elapsed = Date.now() - lastActivity
@@ -582,11 +568,6 @@ class Workers extends EventEmitter {
582
568
  // Track current test
583
569
  if (message.event === event.test.started && message.data) {
584
570
  currentTest = message.data.title || message.data.fullTitle
585
- console.log(`[Worker ${message.workerIndex}] Started: ${currentTest}`)
586
- }
587
-
588
- if (message.event === event.test.finished && message.data) {
589
- console.log(`[Worker ${message.workerIndex}] Finished: ${message.data.title || currentTest}`)
590
571
  }
591
572
 
592
573
  output.process(message.workerIndex)
@@ -627,15 +608,41 @@ class Workers extends EventEmitter {
627
608
  })
628
609
  }
629
610
 
611
+ const exitTimeout = parseInt(process.env.CODECEPT_AUTO_EXIT_TIMEOUT, 10)
612
+ if (exitTimeout === 0) break
613
+ setTimeout(() => {
614
+ autoTerminated = true
615
+ worker.terminate()
616
+ }, exitTimeout || 2000)
617
+
630
618
  break
631
619
  case event.suite.before:
632
- this.emit(event.suite.before, deserializeSuite(message.data))
620
+ {
621
+ const suite = deserializeSuite(message.data)
622
+ this.emit(event.suite.before, suite)
623
+ event.dispatcher.emit(event.suite.before, suite)
624
+ }
625
+ break
626
+ case event.suite.after:
627
+ {
628
+ const suite = deserializeSuite(message.data)
629
+ this.emit(event.suite.after, suite)
630
+ event.dispatcher.emit(event.suite.after, suite)
631
+ }
633
632
  break
634
633
  case event.test.before:
635
- this.emit(event.test.before, deserializeTest(message.data))
634
+ {
635
+ const test = deserializeTest(message.data)
636
+ this.emit(event.test.before, test)
637
+ event.dispatcher.emit(event.test.before, test)
638
+ }
636
639
  break
637
640
  case event.test.started:
638
- this.emit(event.test.started, deserializeTest(message.data))
641
+ {
642
+ const test = deserializeTest(message.data)
643
+ this.emit(event.test.started, test)
644
+ event.dispatcher.emit(event.test.started, test)
645
+ }
639
646
  break
640
647
  case event.test.failed:
641
648
  // For hook failures, emit immediately as there won't be a test.finished event
@@ -649,7 +656,11 @@ class Workers extends EventEmitter {
649
656
  // Skip individual passed events - we'll emit based on finished state
650
657
  break
651
658
  case event.test.skipped:
652
- this.emit(event.test.skipped, deserializeTest(message.data))
659
+ {
660
+ const test = deserializeTest(message.data)
661
+ this.emit(event.test.skipped, test)
662
+ event.dispatcher.emit(event.test.skipped, test)
663
+ }
653
664
  break
654
665
  case event.test.finished:
655
666
  // Handle different types of test completion properly
@@ -678,28 +689,47 @@ class Workers extends EventEmitter {
678
689
  }
679
690
  }
680
691
 
681
- this.emit(event.test.finished, deserializeTest(data))
692
+ const test = deserializeTest(data)
693
+ this.emit(event.test.finished, test)
694
+ event.dispatcher.emit(event.test.finished, test)
682
695
  }
683
696
  break
684
697
  case event.test.after:
685
- this.emit(event.test.after, deserializeTest(message.data))
698
+ {
699
+ const test = deserializeTest(message.data)
700
+ this.emit(event.test.after, test)
701
+ event.dispatcher.emit(event.test.after, test)
702
+ }
686
703
  break
687
704
  case event.step.finished:
688
705
  this.emit(event.step.finished, message.data)
706
+ event.dispatcher.emit(event.step.finished, message.data)
689
707
  break
690
708
  case event.step.started:
691
709
  this.emit(event.step.started, message.data)
710
+ event.dispatcher.emit(event.step.started, message.data)
692
711
  break
693
712
  case event.step.passed:
694
713
  this.emit(event.step.passed, message.data)
714
+ event.dispatcher.emit(event.step.passed, message.data)
695
715
  break
696
716
  case event.step.failed:
697
717
  this.emit(event.step.failed, message.data, message.data.error)
718
+ event.dispatcher.emit(event.step.failed, message.data, message.data.error)
698
719
  break
699
720
  case event.hook.failed:
700
721
  // Hook failures are already reported as test failures by the worker
701
722
  // Just emit the hook.failed event for listeners
702
723
  this.emit(event.hook.failed, message.data)
724
+ event.dispatcher.emit(event.hook.failed, message.data)
725
+ break
726
+ case event.hook.passed:
727
+ this.emit(event.hook.passed, message.data)
728
+ event.dispatcher.emit(event.hook.passed, message.data)
729
+ break
730
+ case event.hook.finished:
731
+ this.emit(event.hook.finished, message.data)
732
+ event.dispatcher.emit(event.hook.finished, message.data)
703
733
  break
704
734
  }
705
735
  })
@@ -715,8 +745,8 @@ class Workers extends EventEmitter {
715
745
  worker.on('exit', (code) => {
716
746
  clearInterval(timeoutChecker)
717
747
  this.closedWorkers += 1
718
-
719
- if (code !== 0) {
748
+
749
+ if (code !== 0 && !autoTerminated) {
720
750
  console.error(`[Main] Worker exited with code ${code}`)
721
751
  if (currentTest) {
722
752
  console.error(`[Main] Last test running: ${currentTest}`)
@@ -724,8 +754,6 @@ class Workers extends EventEmitter {
724
754
  // Mark as failed
725
755
  process.exitCode = 1
726
756
  }
727
-
728
- console.log(`[Main] Workers closed: ${this.closedWorkers}/${this.numberOfWorkers}`)
729
757
 
730
758
  if (this.isPoolMode) {
731
759
  // Pool mode: finish when all workers have exited and no more tests
@@ -740,7 +768,6 @@ class Workers extends EventEmitter {
740
768
  }
741
769
 
742
770
  _finishRun() {
743
- console.log('[Main] Finishing test run...')
744
771
  event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
745
772
  if (Container.result().hasFailed || this.errors.length > 0) {
746
773
  process.exitCode = 1
@@ -782,10 +809,8 @@ class Workers extends EventEmitter {
782
809
  this._testStates.clear()
783
810
  }
784
811
 
785
- console.log('[Main] Emitting final results...')
786
812
  this.emit(event.all.result, Container.result())
787
813
  event.dispatcher.emit(event.workers.result, Container.result())
788
- console.log('[Main] Emitting end event...')
789
814
  this.emit('end') // internal event
790
815
  }
791
816