codeceptjs 4.0.0-rc.8 → 4.0.0

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 (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  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 +743 -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 +198 -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 +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +195 -3
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +96 -115
  126. package/lib/helper/Puppeteer.js +43 -131
  127. package/lib/helper/WebDriver.js +42 -52
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +58 -0
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +15 -2
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +19 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -0,0 +1,151 @@
1
+ import Container from '../container.js'
2
+ import output from '../output.js'
3
+
4
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
5
+
6
+ const RESERVED_KEYS = new Set(['on', 'path', 'line', 'pattern'])
7
+ const ALL_MODES = ['fail', 'test', 'step', 'file', 'url']
8
+
9
+ /**
10
+ * Parse a plugin's _args (from CLI `-p plugin:key=value:key=value`) into a flat dict.
11
+ * Each entry is split on `;` then on the first `=`. Bare segments become `{ key: true }`.
12
+ *
13
+ * Examples:
14
+ * parsePluginArgs(['on=fail'])
15
+ * → { on: 'fail' }
16
+ * parsePluginArgs(['on=file', 'path=tests/foo.js;line=43'])
17
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
18
+ * parsePluginArgs(['on=file', 'path=tests/foo.js', 'line=43'])
19
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
20
+ * parsePluginArgs(['show'])
21
+ * → { show: true }
22
+ */
23
+ export function parsePluginArgs(args = []) {
24
+ const opts = {}
25
+ for (const arg of args) {
26
+ if (!arg) continue
27
+ for (const segment of arg.split(';')) {
28
+ if (!segment) continue
29
+ if (segment.includes('=')) {
30
+ const eq = segment.indexOf('=')
31
+ const key = segment.slice(0, eq)
32
+ const value = segment.slice(eq + 1)
33
+ opts[key] = coerce(value)
34
+ } else {
35
+ opts[segment] = true
36
+ }
37
+ }
38
+ }
39
+ return opts
40
+ }
41
+
42
+ function coerce(v) {
43
+ if (v === 'true') return true
44
+ if (v === 'false') return false
45
+ return v
46
+ }
47
+
48
+ /**
49
+ * Compose CLI args > config > defaults into a normalized trigger spec, then
50
+ * validate it. Returns `{ on, path, line, pattern, ...rest }` with `line`
51
+ * coerced to a number, or `null` if validation failed (an error is printed).
52
+ *
53
+ * @param {object} cliArgs — output of parsePluginArgs(config._args)
54
+ * @param {object} config — full plugin config object
55
+ * @param {object} defaults — fallback values, e.g. `{ on: 'fail' }`
56
+ * @param {object} options
57
+ * @param {string} options.name — plugin name, used in error messages
58
+ * @param {string[]} [options.validModes] — accepted values for `on`
59
+ * (default: fail, test, step, file, url)
60
+ */
61
+ export function resolveTrigger(cliArgs = {}, config = {}, defaults = {}, options = {}) {
62
+ const { name = 'plugin', validModes = ALL_MODES } = options
63
+ const merged = { ...defaults, ...pickKnown(config), ...cliArgs }
64
+ if (merged.line != null) merged.line = parseInt(merged.line, 10)
65
+
66
+ const valid = new Set(validModes)
67
+ if (!valid.has(merged.on)) {
68
+ output.error(`${name}: unknown on="${merged.on}". Valid: ${validModes.join(', ')}`)
69
+ return null
70
+ }
71
+ if (merged.on === 'file' && !merged.path) {
72
+ output.error(`${name}:on=file requires path=. Example: -p ${name}:on=file:path=tests/foo.js`)
73
+ return null
74
+ }
75
+ if (merged.on === 'url' && !merged.pattern) {
76
+ output.error(`${name}:on=url requires pattern=. Example: -p ${name}:on=url:pattern=/users/*`)
77
+ return null
78
+ }
79
+
80
+ return merged
81
+ }
82
+
83
+ function pickKnown(config) {
84
+ const out = {}
85
+ for (const key of Object.keys(config || {})) {
86
+ if (RESERVED_KEYS.has(key)) out[key] = config[key]
87
+ }
88
+ return out
89
+ }
90
+
91
+ /**
92
+ * Match a step's source location against a `path` (substring/suffix) and optional `line`.
93
+ * Reads the step's stack via `step.line()` to get `file:row:col`.
94
+ */
95
+ export function matchStepFile(step, targetPath, targetLine) {
96
+ if (!targetPath) return false
97
+ const stepLine = step.line && step.line()
98
+ if (!stepLine) return false
99
+
100
+ const parsed = parseStepLine(stepLine)
101
+ if (!parsed) return false
102
+
103
+ const fileMatches = parsed.file.includes(targetPath) || parsed.file.endsWith(targetPath)
104
+ if (!fileMatches) return false
105
+
106
+ if (targetLine != null && !Number.isNaN(targetLine) && parsed.line !== targetLine) return false
107
+ return true
108
+ }
109
+
110
+ function parseStepLine(stepLine) {
111
+ let line = stepLine.trim()
112
+ if (line.startsWith('at ')) line = line.substring(3).trim()
113
+
114
+ const lastColon = line.lastIndexOf(':')
115
+ if (lastColon < 0) return null
116
+ const secondLastColon = line.lastIndexOf(':', lastColon - 1)
117
+ if (secondLastColon < 0) return null
118
+
119
+ const file = line.substring(0, secondLastColon)
120
+ const lineNum = parseInt(line.substring(secondLastColon + 1, lastColon), 10)
121
+
122
+ if (Number.isNaN(lineNum)) return null
123
+ return { file, line: lineNum }
124
+ }
125
+
126
+ /**
127
+ * Match a URL string against a glob-style pattern (supports `*` wildcards).
128
+ */
129
+ export function matchUrl(currentUrl, pattern) {
130
+ if (!pattern || !currentUrl) return false
131
+ return patternToRegex(pattern).test(currentUrl)
132
+ }
133
+
134
+ function patternToRegex(pattern) {
135
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
136
+ const regexStr = escaped.replace(/\*/g, '.*')
137
+ return new RegExp(regexStr)
138
+ }
139
+
140
+ /**
141
+ * Return the first available standard browser helper, or null.
142
+ */
143
+ export function getBrowserHelper() {
144
+ const helpers = Container.helpers()
145
+ for (const name of supportedHelpers) {
146
+ if (Object.keys(helpers).indexOf(name) > -1) {
147
+ return helpers[name]
148
+ }
149
+ }
150
+ return null
151
+ }
@@ -0,0 +1,297 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { pathToFileURL } from 'url'
5
+ import Container from '../container.js'
6
+ import { clearString } from '../utils.js'
7
+ import { formatHtml } from '../html.js'
8
+ import { diffAriaSnapshots } from '../aria.js'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helper / directory naming
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function pickActingHelper(helpers) {
15
+ for (const name of Container.STANDARD_ACTING_HELPERS) {
16
+ if (helpers[name]) return helpers[name]
17
+ }
18
+ return null
19
+ }
20
+
21
+ export function traceDirFor(testFile, testTitle, baseDir) {
22
+ const hash = crypto.createHash('sha256').update((testFile || '') + (testTitle || '')).digest('hex').slice(0, 8)
23
+ const cleanTitle = clearString(testTitle || '').slice(0, 200)
24
+ return path.resolve(baseDir, `trace_${cleanTitle}_${hash}`)
25
+ }
26
+
27
+ export function snapshotDirFor(baseDir) {
28
+ const hash = crypto.randomBytes(4).toString('hex')
29
+ return path.resolve(baseDir, `snapshot_${Date.now()}_${hash}`)
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Artifact link rendering (trace.md)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const ARTIFACT_LABELS = {
37
+ html: 'HTML',
38
+ aria: 'ARIA',
39
+ screenshot: 'Screenshot',
40
+ console: 'Browser Logs',
41
+ storage: 'Storage',
42
+ }
43
+
44
+ export function artifactLinks(artifacts, { indent = ' ', consoleCount } = {}) {
45
+ const lines = []
46
+ const order = ['html', 'aria', 'screenshot', 'console', 'storage']
47
+
48
+ for (const key of order) {
49
+ const file = artifacts[key]
50
+ if (!file) continue
51
+ const label = ARTIFACT_LABELS[key]
52
+ let line = `${indent}> [${label}](./${file})`
53
+ if (key === 'console') {
54
+ const count = consoleCount ?? artifacts.consoleCount ?? 0
55
+ line += ` (${count} entries)`
56
+ } else if (key === 'storage') {
57
+ const cookies = artifacts.cookieCount ?? 0
58
+ const ls = artifacts.localStorageCount ?? 0
59
+ line += ` (${cookies} cookies, ${ls} localStorage)`
60
+ }
61
+ lines.push(line)
62
+ }
63
+
64
+ return lines.join('\n')
65
+ }
66
+
67
+ export function fileToUrl(dir, basename) {
68
+ return pathToFileURL(path.join(dir, basename)).href
69
+ }
70
+
71
+ export function writeTraceMarkdown({ dir, title, file, durationMs, commands, captured, error }) {
72
+ let md = `file: ${file || 'mcp'}\n`
73
+ md += `name: ${title}\n`
74
+ md += `time: ${(durationMs / 1000).toFixed(2)}s\n`
75
+ md += `---\n\n`
76
+
77
+ if (error) md += `Error: ${error}\n\n---\n\n`
78
+
79
+ if (commands && commands.length) {
80
+ md += `### Commands\n`
81
+ for (const c of commands) md += `- ${c}\n`
82
+ md += `\n`
83
+ }
84
+
85
+ md += `### Final State\n`
86
+ if (captured.url) md += ` > URL: ${captured.url}\n`
87
+ const links = artifactLinks(captured)
88
+ if (links) md += links + '\n'
89
+
90
+ const traceFile = path.join(dir, 'trace.md')
91
+ fs.writeFileSync(traceFile, md)
92
+ return traceFile
93
+ }
94
+
95
+ export function artifactsToFileUrls(captured, dir) {
96
+ const out = {}
97
+ if (captured.url) out.url = captured.url
98
+ if (captured.screenshot) out.screenshot = fileToUrl(dir, captured.screenshot)
99
+ if (captured.html) out.html = fileToUrl(dir, captured.html)
100
+ if (captured.aria) out.aria = fileToUrl(dir, captured.aria)
101
+ if (captured.console) out.console = fileToUrl(dir, captured.console)
102
+ if (captured.storage) out.storage = fileToUrl(dir, captured.storage)
103
+ if (typeof captured.consoleCount === 'number') out.consoleCount = captured.consoleCount
104
+ if (typeof captured.cookieCount === 'number') out.cookieCount = captured.cookieCount
105
+ if (typeof captured.localStorageCount === 'number') out.localStorageCount = captured.localStorageCount
106
+ return out
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Snapshot capture (HTML / ARIA / screenshot / console / storage)
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function normalizeBrowserLogs(logs) {
114
+ return (logs || []).map(l => {
115
+ if (typeof l === 'string') return l
116
+ if (l && typeof l.type === 'function' && typeof l.text === 'function') {
117
+ return { type: l.type(), text: l.text() }
118
+ }
119
+ return l
120
+ })
121
+ }
122
+
123
+ async function captureStorageState(helper) {
124
+ if (typeof helper.grabStorageState === 'function') {
125
+ try {
126
+ const state = await helper.grabStorageState()
127
+ if (state) return state
128
+ } catch {}
129
+ }
130
+
131
+ const state = { cookies: [], origins: [] }
132
+
133
+ if (typeof helper.grabCookie === 'function') {
134
+ try {
135
+ const cookies = await helper.grabCookie()
136
+ if (Array.isArray(cookies)) state.cookies = cookies
137
+ } catch {}
138
+ }
139
+
140
+ if (typeof helper.executeScript === 'function') {
141
+ try {
142
+ const result = await helper.executeScript(() => {
143
+ const out = { origin: location.origin, items: [] }
144
+ for (let i = 0; i < localStorage.length; i++) {
145
+ const name = localStorage.key(i)
146
+ out.items.push({ name, value: localStorage.getItem(name) })
147
+ }
148
+ return out
149
+ })
150
+ if (result?.items?.length) {
151
+ state.origins.push({ origin: result.origin, localStorage: result.items })
152
+ }
153
+ } catch {}
154
+ }
155
+
156
+ return state
157
+ }
158
+
159
+ export async function captureSnapshot(helper, {
160
+ dir,
161
+ prefix = 'snapshot',
162
+ fullPage = false,
163
+ captureURL = true,
164
+ captureScreenshot = true,
165
+ captureHTML = true,
166
+ captureARIA = true,
167
+ captureBrowserLogs = true,
168
+ captureStorage = true,
169
+ } = {}) {
170
+ if (!helper) return {}
171
+ const out = {}
172
+
173
+ if (captureURL) {
174
+ try {
175
+ if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl()
176
+ } catch {}
177
+ }
178
+
179
+ if (captureScreenshot && helper.saveScreenshot) {
180
+ try {
181
+ const file = `${prefix}_screenshot.png`
182
+ await helper.saveScreenshot(path.join(dir, file), fullPage)
183
+ out.screenshot = file
184
+ } catch {}
185
+ }
186
+
187
+ if (captureHTML && helper.grabSource) {
188
+ try {
189
+ const html = await helper.grabSource()
190
+ // Universal funnel: every captured HTML snapshot flows through formatHtml
191
+ // (minify -> cleanHtml -> beautify). Don't add direct grabSource->writeFile
192
+ // paths elsewhere; route through this util so trash-class cleanup stays
193
+ // consistent across aiTrace, pageInfo, and MCP tools.
194
+ const formatted = await formatHtml(html)
195
+ const file = `${prefix}_page.html`
196
+ fs.writeFileSync(path.join(dir, file), formatted)
197
+ out.html = file
198
+ // Expose pre-cleanup HTML for consumers that need to inspect classes
199
+ // stripped by cleanHtml (e.g. pageInfo's error-class scan).
200
+ out.htmlRaw = html
201
+ } catch {}
202
+ }
203
+
204
+ if (captureARIA && helper.grabAriaSnapshot) {
205
+ try {
206
+ const aria = await helper.grabAriaSnapshot()
207
+ const file = `${prefix}_aria.txt`
208
+ fs.writeFileSync(path.join(dir, file), aria)
209
+ out.aria = file
210
+ } catch {}
211
+ }
212
+
213
+ if (captureBrowserLogs && helper.grabBrowserLogs) {
214
+ try {
215
+ const logs = await helper.grabBrowserLogs()
216
+ const normalized = normalizeBrowserLogs(logs)
217
+ const file = `${prefix}_console.json`
218
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(normalized, null, 2))
219
+ out.console = file
220
+ out.consoleCount = normalized.length
221
+ } catch {}
222
+ }
223
+
224
+ if (captureStorage) {
225
+ try {
226
+ const state = await captureStorageState(helper)
227
+ const cookieCount = state.cookies?.length || 0
228
+ const localStorageCount = (state.origins || [])
229
+ .reduce((sum, o) => sum + (o.localStorage?.length || 0), 0)
230
+ if (cookieCount || localStorageCount) {
231
+ const file = `${prefix}_storage.json`
232
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(state, null, 2))
233
+ out.storage = file
234
+ out.cookieCount = cookieCount
235
+ out.localStorageCount = localStorageCount
236
+ }
237
+ } catch {}
238
+ }
239
+
240
+ return out
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // TraceReader — read artifacts already on disk (written by aiTrace, MCP, etc.)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ const KIND_SUFFIX = {
248
+ aria: '_aria.txt',
249
+ html: '_page.html',
250
+ screenshot: '_screenshot.png',
251
+ console: '_console.json',
252
+ storage: '_storage.json',
253
+ }
254
+
255
+ export class TraceReader {
256
+ constructor(dir) {
257
+ this.dir = dir
258
+ }
259
+
260
+ // Filenames of a given kind, sorted in capture order. aiTrace prefixes with
261
+ // a zero-padded step index (`0000_`, `0001_`...), so a lexical sort is
262
+ // chronological.
263
+ list(kind) {
264
+ const suffix = KIND_SUFFIX[kind]
265
+ if (!suffix || !this.dir || !fs.existsSync(this.dir)) return []
266
+ let entries
267
+ try { entries = fs.readdirSync(this.dir) } catch { return [] }
268
+ return entries.filter(f => f.endsWith(suffix)).sort()
269
+ }
270
+
271
+ // Path of the n-th file of `kind`, or null. Python-style indexing:
272
+ // 0..N-1 from the start, -1..-N from the end.
273
+ pathAt(n, kind) {
274
+ const files = this.list(kind)
275
+ if (!files.length) return null
276
+ const i = n < 0 ? files.length + n : n
277
+ if (i < 0 || i >= files.length) return null
278
+ return path.join(this.dir, files[i])
279
+ }
280
+
281
+ // Read content of the n-th file of `kind`. Binary kinds (screenshot) are
282
+ // returned as Buffer; text kinds as utf8 string.
283
+ nth(n, kind) {
284
+ const p = this.pathAt(n, kind)
285
+ if (!p) return null
286
+ try {
287
+ if (kind === 'screenshot') return fs.readFileSync(p)
288
+ return fs.readFileSync(p, 'utf8')
289
+ } catch { return null }
290
+ }
291
+
292
+ first(kind) { return this.nth(0, kind) }
293
+ last(kind) { return this.nth(-1, kind) }
294
+ count(kind) { return this.list(kind).length }
295
+ }
296
+
297
+ export const ariaDiff = diffAriaSnapshots
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'
@@ -335,13 +336,13 @@ export const screenshotOutputFolder = function (fileName) {
335
336
  const fileSep = path.sep
336
337
 
337
338
  if (!fileName.includes(fileSep) || fileName.includes('record_')) {
338
- return path.resolve(global.output_dir, fileName)
339
+ return path.resolve(store.outputDir, fileName)
339
340
  }
340
- return path.resolve(global.codecept_dir, fileName)
341
+ return path.resolve(store.codeceptDir, fileName)
341
342
  }
342
343
 
343
344
  export const relativeDir = function (fileName) {
344
- return fileName.replace(global.codecept_dir, '').replace(/^\//, '')
345
+ return fileName.replace(store.codeceptDir, '').replace(/^\//, '')
345
346
  }
346
347
 
347
348
  export const beautify = function (code) {
@@ -616,6 +617,12 @@ function createCircularSafeReplacer(keysToSkip = []) {
616
617
  return undefined
617
618
  }
618
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
+
619
626
  if (value === null || typeof value !== 'object') {
620
627
  return value
621
628
  }
@@ -646,6 +653,25 @@ export const safeStringify = function (obj, keysToSkip = [], space = 0) {
646
653
  }
647
654
  }
648
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
+
649
675
  export const serializeError = function (error) {
650
676
  if (error) {
651
677
  const { stack, uncaught, message, actual, expected } = error
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'
@@ -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')),
@@ -504,6 +501,7 @@ class Workers extends EventEmitter {
504
501
  await this._ensureInitialized()
505
502
  recorder.startUnlessRunning()
506
503
  event.dispatcher.emit(event.workers.before)
504
+ store.workerMode = true
507
505
  process.env.RUNS_WITH_WORKERS = 'true'
508
506
 
509
507
  // Create workers and set up message handlers immediately (not in recorder queue)
@@ -519,22 +517,8 @@ class Workers extends EventEmitter {
519
517
  // Workers are already running, this is just a placeholder step
520
518
  })
521
519
 
522
- // Add overall timeout to prevent infinite hanging
523
- const overallTimeout = setTimeout(() => {
524
- console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
525
- workerThreads.forEach(w => {
526
- try {
527
- w.terminate()
528
- } catch (e) {
529
- // ignore
530
- }
531
- })
532
- this._finishRun()
533
- }, 600000) // 10 minutes
534
-
535
520
  return new Promise(resolve => {
536
521
  this.on('end', () => {
537
- clearTimeout(overallTimeout)
538
522
  resolve()
539
523
  })
540
524
  })
@@ -559,11 +543,12 @@ class Workers extends EventEmitter {
559
543
  if (this.isPoolMode) {
560
544
  this.activeWorkers.set(worker, { available: true, workerIndex: null })
561
545
  }
562
-
546
+
563
547
  // Track last activity time to detect hanging workers
564
548
  let lastActivity = Date.now()
565
549
  let currentTest = null
566
- 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')
567
552
 
568
553
  const timeoutChecker = setInterval(() => {
569
554
  const elapsed = Date.now() - lastActivity
@@ -623,6 +608,13 @@ class Workers extends EventEmitter {
623
608
  })
624
609
  }
625
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
+
626
618
  break
627
619
  case event.suite.before:
628
620
  {
@@ -753,8 +745,8 @@ class Workers extends EventEmitter {
753
745
  worker.on('exit', (code) => {
754
746
  clearInterval(timeoutChecker)
755
747
  this.closedWorkers += 1
756
-
757
- if (code !== 0) {
748
+
749
+ if (code !== 0 && !autoTerminated) {
758
750
  console.error(`[Main] Worker exited with code ${code}`)
759
751
  if (currentTest) {
760
752
  console.error(`[Main] Last test running: ${currentTest}`)