codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1187 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +159 -0
  7. package/docs/ai.md +537 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/assertions.md +415 -0
  11. package/docs/auth.md +318 -0
  12. package/docs/basics.md +424 -0
  13. package/docs/bdd.md +539 -0
  14. package/docs/best.md +240 -0
  15. package/docs/bootstrap.md +132 -0
  16. package/docs/commands.md +352 -0
  17. package/docs/community-helpers.md +63 -0
  18. package/docs/configuration.md +230 -0
  19. package/docs/continuous-integration.md +497 -0
  20. package/docs/custom-helpers.md +297 -0
  21. package/docs/data.md +448 -0
  22. package/docs/debugging.md +332 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/docker.md +136 -0
  25. package/docs/effects.md +179 -0
  26. package/docs/element-based-testing.md +295 -0
  27. package/docs/element-selection.md +125 -0
  28. package/docs/els.md +328 -0
  29. package/docs/examples.md +161 -0
  30. package/docs/heal.md +213 -0
  31. package/docs/helpers/ApiDataFactory.md +267 -0
  32. package/docs/helpers/Appium.md +1405 -0
  33. package/docs/helpers/Detox.md +665 -0
  34. package/docs/helpers/ExpectHelper.md +275 -0
  35. package/docs/helpers/FileSystem.md +152 -0
  36. package/docs/helpers/GraphQL.md +152 -0
  37. package/docs/helpers/GraphQLDataFactory.md +226 -0
  38. package/docs/helpers/JSONResponse.md +255 -0
  39. package/docs/helpers/Mochawesome.md +8 -0
  40. package/docs/helpers/MockRequest.md +377 -0
  41. package/docs/helpers/MockServer.md +212 -0
  42. package/docs/helpers/Playwright.md +2969 -0
  43. package/docs/helpers/Polly.md +44 -0
  44. package/docs/helpers/Protractor.md +1769 -0
  45. package/docs/helpers/Puppeteer-firefox.md +86 -0
  46. package/docs/helpers/Puppeteer.md +2690 -0
  47. package/docs/helpers/REST.md +289 -0
  48. package/docs/helpers/SoftExpectHelper.md +352 -0
  49. package/docs/helpers/WebDriver.md +2682 -0
  50. package/docs/hooks.md +339 -0
  51. package/docs/index.md +111 -0
  52. package/docs/installation.md +83 -0
  53. package/docs/internal-api.md +265 -0
  54. package/docs/internal-test-server.md +89 -0
  55. package/docs/locators.md +355 -0
  56. package/docs/mcp.md +485 -0
  57. package/docs/migration-4.md +556 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +585 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins.md +866 -0
  63. package/docs/puppeteer.md +314 -0
  64. package/docs/quickstart.md +120 -0
  65. package/docs/react.md +70 -0
  66. package/docs/reports.md +483 -0
  67. package/docs/retry.md +274 -0
  68. package/docs/secrets.md +150 -0
  69. package/docs/sessions.md +80 -0
  70. package/docs/shadow.md +68 -0
  71. package/docs/test-structure.md +275 -0
  72. package/docs/timeouts.md +183 -0
  73. package/docs/translation.md +247 -0
  74. package/docs/tutorial.md +271 -0
  75. package/docs/typescript.md +374 -0
  76. package/docs/web-element.md +251 -0
  77. package/docs/webdriver.md +708 -0
  78. package/docs/within.md +55 -0
  79. package/lib/ai.js +3 -2
  80. package/lib/aria.js +260 -0
  81. package/lib/assertions.js +18 -0
  82. package/lib/codecept.js +26 -23
  83. package/lib/command/check.js +2 -1
  84. package/lib/command/dryRun.js +24 -5
  85. package/lib/command/generate.js +2 -0
  86. package/lib/command/gherkin/snippets.js +5 -4
  87. package/lib/command/init.js +248 -269
  88. package/lib/command/list.js +150 -10
  89. package/lib/command/query.js +218 -0
  90. package/lib/command/run-multiple.js +2 -0
  91. package/lib/command/run-workers.js +2 -0
  92. package/lib/command/run.js +1 -1
  93. package/lib/command/workers/runTests.js +10 -10
  94. package/lib/config.js +77 -4
  95. package/lib/container.js +114 -17
  96. package/lib/effects.js +17 -0
  97. package/lib/element/WebElement.js +246 -2
  98. package/lib/els.js +12 -6
  99. package/lib/globals.js +32 -19
  100. package/lib/heal.js +4 -3
  101. package/lib/helper/ApiDataFactory.js +2 -1
  102. package/lib/helper/Appium.js +8 -8
  103. package/lib/helper/FileSystem.js +3 -2
  104. package/lib/helper/GraphQLDataFactory.js +2 -1
  105. package/lib/helper/Playwright.js +228 -162
  106. package/lib/helper/Puppeteer.js +208 -76
  107. package/lib/helper/WebDriver.js +173 -68
  108. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  109. package/lib/helper/errors/NonFocusedType.js +8 -0
  110. package/lib/helper/extras/Download.js +45 -0
  111. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  112. package/lib/helper/extras/elementSelection.js +58 -0
  113. package/lib/helper/extras/focusCheck.js +43 -0
  114. package/lib/helper/extras/richTextEditor.js +178 -0
  115. package/lib/helper/scripts/dropFile.js +11 -0
  116. package/lib/history.js +3 -2
  117. package/lib/html.js +103 -16
  118. package/lib/index.js +9 -1
  119. package/lib/listener/config.js +6 -4
  120. package/lib/listener/emptyRun.js +2 -1
  121. package/lib/listener/globalRetry.js +32 -6
  122. package/lib/listener/helpers.js +4 -1
  123. package/lib/listener/mocha.js +2 -1
  124. package/lib/listener/pageobjects.js +43 -0
  125. package/lib/listener/result.js +3 -2
  126. package/lib/locator.js +126 -3
  127. package/lib/mocha/cli.js +14 -2
  128. package/lib/mocha/factory.js +7 -2
  129. package/lib/mocha/inject.js +1 -1
  130. package/lib/mocha/scenarioConfig.js +2 -1
  131. package/lib/mocha/ui.js +5 -6
  132. package/lib/parser.js +2 -2
  133. package/lib/pause.js +38 -4
  134. package/lib/plugin/aiTrace.js +453 -0
  135. package/lib/plugin/analyze.js +1 -1
  136. package/lib/plugin/auth.js +3 -3
  137. package/lib/plugin/browser.js +77 -0
  138. package/lib/plugin/expose.js +159 -0
  139. package/lib/plugin/heal.js +44 -1
  140. package/lib/plugin/pageInfo.js +53 -49
  141. package/lib/plugin/pause.js +131 -0
  142. package/lib/plugin/pauseOnFail.js +10 -34
  143. package/lib/plugin/retryFailedStep.js +28 -19
  144. package/lib/plugin/screencast.js +287 -0
  145. package/lib/plugin/screenshot.js +563 -0
  146. package/lib/plugin/screenshotOnFail.js +8 -171
  147. package/lib/rerun.js +2 -1
  148. package/lib/result.js +2 -1
  149. package/lib/step/base.js +3 -2
  150. package/lib/step/config.js +15 -2
  151. package/lib/step/record.js +2 -2
  152. package/lib/store.js +72 -3
  153. package/lib/translation.js +2 -1
  154. package/lib/utils/mask_data.js +2 -1
  155. package/lib/utils/pluginParser.js +151 -0
  156. package/lib/utils/trace.js +297 -0
  157. package/lib/utils.js +77 -3
  158. package/lib/workers.js +52 -22
  159. package/package.json +19 -13
  160. package/typings/index.d.ts +19 -5
  161. package/docs/webapi/amOnPage.mustache +0 -11
  162. package/docs/webapi/appendField.mustache +0 -11
  163. package/docs/webapi/attachFile.mustache +0 -12
  164. package/docs/webapi/blur.mustache +0 -18
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -9
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -29
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  177. package/docs/webapi/dontSeeElement.mustache +0 -8
  178. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  179. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  180. package/docs/webapi/dontSeeInField.mustache +0 -11
  181. package/docs/webapi/dontSeeInSource.mustache +0 -8
  182. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  183. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  184. package/docs/webapi/doubleClick.mustache +0 -13
  185. package/docs/webapi/downloadFile.mustache +0 -12
  186. package/docs/webapi/dragAndDrop.mustache +0 -9
  187. package/docs/webapi/dragSlider.mustache +0 -11
  188. package/docs/webapi/executeAsyncScript.mustache +0 -24
  189. package/docs/webapi/executeScript.mustache +0 -26
  190. package/docs/webapi/fillField.mustache +0 -16
  191. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  192. package/docs/webapi/focus.mustache +0 -13
  193. package/docs/webapi/forceClick.mustache +0 -28
  194. package/docs/webapi/forceRightClick.mustache +0 -18
  195. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  196. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  197. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  198. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  199. package/docs/webapi/grabCookie.mustache +0 -11
  200. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  201. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  202. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  203. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  204. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  205. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  206. package/docs/webapi/grabGeoLocation.mustache +0 -8
  207. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  208. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  209. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  210. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  211. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  212. package/docs/webapi/grabPopupText.mustache +0 -5
  213. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  214. package/docs/webapi/grabSource.mustache +0 -8
  215. package/docs/webapi/grabTextFrom.mustache +0 -10
  216. package/docs/webapi/grabTextFromAll.mustache +0 -9
  217. package/docs/webapi/grabTitle.mustache +0 -8
  218. package/docs/webapi/grabValueFrom.mustache +0 -9
  219. package/docs/webapi/grabValueFromAll.mustache +0 -8
  220. package/docs/webapi/grabWebElement.mustache +0 -9
  221. package/docs/webapi/grabWebElements.mustache +0 -9
  222. package/docs/webapi/moveCursorTo.mustache +0 -12
  223. package/docs/webapi/openNewTab.mustache +0 -7
  224. package/docs/webapi/pressKey.mustache +0 -12
  225. package/docs/webapi/pressKeyDown.mustache +0 -12
  226. package/docs/webapi/pressKeyUp.mustache +0 -12
  227. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  228. package/docs/webapi/refreshPage.mustache +0 -6
  229. package/docs/webapi/resizeWindow.mustache +0 -6
  230. package/docs/webapi/rightClick.mustache +0 -14
  231. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  232. package/docs/webapi/saveScreenshot.mustache +0 -12
  233. package/docs/webapi/say.mustache +0 -10
  234. package/docs/webapi/scrollIntoView.mustache +0 -11
  235. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  236. package/docs/webapi/scrollPageToTop.mustache +0 -6
  237. package/docs/webapi/scrollTo.mustache +0 -12
  238. package/docs/webapi/see.mustache +0 -11
  239. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  240. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  241. package/docs/webapi/seeCookie.mustache +0 -8
  242. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  243. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  244. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  245. package/docs/webapi/seeElement.mustache +0 -8
  246. package/docs/webapi/seeElementInDOM.mustache +0 -8
  247. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  248. package/docs/webapi/seeInField.mustache +0 -12
  249. package/docs/webapi/seeInPopup.mustache +0 -8
  250. package/docs/webapi/seeInSource.mustache +0 -7
  251. package/docs/webapi/seeInTitle.mustache +0 -8
  252. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  253. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  254. package/docs/webapi/seeTextEquals.mustache +0 -9
  255. package/docs/webapi/seeTitleEquals.mustache +0 -8
  256. package/docs/webapi/seeTraffic.mustache +0 -36
  257. package/docs/webapi/selectOption.mustache +0 -21
  258. package/docs/webapi/setCookie.mustache +0 -16
  259. package/docs/webapi/setGeoLocation.mustache +0 -12
  260. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  261. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  262. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  263. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  264. package/docs/webapi/switchTo.mustache +0 -9
  265. package/docs/webapi/switchToNextTab.mustache +0 -10
  266. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  267. package/docs/webapi/type.mustache +0 -21
  268. package/docs/webapi/uncheckOption.mustache +0 -13
  269. package/docs/webapi/wait.mustache +0 -8
  270. package/docs/webapi/waitForClickable.mustache +0 -11
  271. package/docs/webapi/waitForCookie.mustache +0 -9
  272. package/docs/webapi/waitForDetached.mustache +0 -10
  273. package/docs/webapi/waitForDisabled.mustache +0 -6
  274. package/docs/webapi/waitForElement.mustache +0 -11
  275. package/docs/webapi/waitForEnabled.mustache +0 -6
  276. package/docs/webapi/waitForFunction.mustache +0 -17
  277. package/docs/webapi/waitForInvisible.mustache +0 -10
  278. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  279. package/docs/webapi/waitForText.mustache +0 -13
  280. package/docs/webapi/waitForValue.mustache +0 -10
  281. package/docs/webapi/waitForVisible.mustache +0 -10
  282. package/docs/webapi/waitInUrl.mustache +0 -9
  283. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  284. package/docs/webapi/waitToHide.mustache +0 -10
  285. package/docs/webapi/waitUrlEquals.mustache +0 -10
  286. package/lib/helper/AI.js +0 -214
  287. package/lib/listener/enhancedGlobalRetry.js +0 -110
  288. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  289. package/lib/plugin/htmlReporter.js +0 -3648
  290. package/lib/plugin/stepByStepReport.js +0 -427
  291. package/lib/plugin/subtitles.js +0 -89
  292. package/lib/retryCoordinator.js +0 -207
  293. package/typings/promiseBasedTypes.d.ts +0 -9469
  294. package/typings/types.d.ts +0 -11402
@@ -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'
@@ -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
 
@@ -504,6 +505,7 @@ class Workers extends EventEmitter {
504
505
  await this._ensureInitialized()
505
506
  recorder.startUnlessRunning()
506
507
  event.dispatcher.emit(event.workers.before)
508
+ store.workerMode = true
507
509
  process.env.RUNS_WITH_WORKERS = 'true'
508
510
 
509
511
  // Create workers and set up message handlers immediately (not in recorder queue)
@@ -519,22 +521,8 @@ class Workers extends EventEmitter {
519
521
  // Workers are already running, this is just a placeholder step
520
522
  })
521
523
 
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
524
  return new Promise(resolve => {
536
525
  this.on('end', () => {
537
- clearTimeout(overallTimeout)
538
526
  resolve()
539
527
  })
540
528
  })
@@ -563,7 +551,7 @@ class Workers extends EventEmitter {
563
551
  // Track last activity time to detect hanging workers
564
552
  let lastActivity = Date.now()
565
553
  let currentTest = null
566
- const workerTimeout = 300000 // 5 minutes
554
+ const workerTimeout = process.env.CODECEPT_WORKER_TIMEOUT ? ms(process.env.CODECEPT_WORKER_TIMEOUT) : ms('5m')
567
555
 
568
556
  const timeoutChecker = setInterval(() => {
569
557
  const elapsed = Date.now() - lastActivity
@@ -625,13 +613,32 @@ class Workers extends EventEmitter {
625
613
 
626
614
  break
627
615
  case event.suite.before:
628
- this.emit(event.suite.before, deserializeSuite(message.data))
616
+ {
617
+ const suite = deserializeSuite(message.data)
618
+ this.emit(event.suite.before, suite)
619
+ event.dispatcher.emit(event.suite.before, suite)
620
+ }
621
+ break
622
+ case event.suite.after:
623
+ {
624
+ const suite = deserializeSuite(message.data)
625
+ this.emit(event.suite.after, suite)
626
+ event.dispatcher.emit(event.suite.after, suite)
627
+ }
629
628
  break
630
629
  case event.test.before:
631
- this.emit(event.test.before, deserializeTest(message.data))
630
+ {
631
+ const test = deserializeTest(message.data)
632
+ this.emit(event.test.before, test)
633
+ event.dispatcher.emit(event.test.before, test)
634
+ }
632
635
  break
633
636
  case event.test.started:
634
- this.emit(event.test.started, deserializeTest(message.data))
637
+ {
638
+ const test = deserializeTest(message.data)
639
+ this.emit(event.test.started, test)
640
+ event.dispatcher.emit(event.test.started, test)
641
+ }
635
642
  break
636
643
  case event.test.failed:
637
644
  // For hook failures, emit immediately as there won't be a test.finished event
@@ -645,7 +652,11 @@ class Workers extends EventEmitter {
645
652
  // Skip individual passed events - we'll emit based on finished state
646
653
  break
647
654
  case event.test.skipped:
648
- this.emit(event.test.skipped, deserializeTest(message.data))
655
+ {
656
+ const test = deserializeTest(message.data)
657
+ this.emit(event.test.skipped, test)
658
+ event.dispatcher.emit(event.test.skipped, test)
659
+ }
649
660
  break
650
661
  case event.test.finished:
651
662
  // Handle different types of test completion properly
@@ -674,28 +685,47 @@ class Workers extends EventEmitter {
674
685
  }
675
686
  }
676
687
 
677
- this.emit(event.test.finished, deserializeTest(data))
688
+ const test = deserializeTest(data)
689
+ this.emit(event.test.finished, test)
690
+ event.dispatcher.emit(event.test.finished, test)
678
691
  }
679
692
  break
680
693
  case event.test.after:
681
- this.emit(event.test.after, deserializeTest(message.data))
694
+ {
695
+ const test = deserializeTest(message.data)
696
+ this.emit(event.test.after, test)
697
+ event.dispatcher.emit(event.test.after, test)
698
+ }
682
699
  break
683
700
  case event.step.finished:
684
701
  this.emit(event.step.finished, message.data)
702
+ event.dispatcher.emit(event.step.finished, message.data)
685
703
  break
686
704
  case event.step.started:
687
705
  this.emit(event.step.started, message.data)
706
+ event.dispatcher.emit(event.step.started, message.data)
688
707
  break
689
708
  case event.step.passed:
690
709
  this.emit(event.step.passed, message.data)
710
+ event.dispatcher.emit(event.step.passed, message.data)
691
711
  break
692
712
  case event.step.failed:
693
713
  this.emit(event.step.failed, message.data, message.data.error)
714
+ event.dispatcher.emit(event.step.failed, message.data, message.data.error)
694
715
  break
695
716
  case event.hook.failed:
696
717
  // Hook failures are already reported as test failures by the worker
697
718
  // Just emit the hook.failed event for listeners
698
719
  this.emit(event.hook.failed, message.data)
720
+ event.dispatcher.emit(event.hook.failed, message.data)
721
+ break
722
+ case event.hook.passed:
723
+ this.emit(event.hook.passed, message.data)
724
+ event.dispatcher.emit(event.hook.passed, message.data)
725
+ break
726
+ case event.hook.finished:
727
+ this.emit(event.hook.finished, message.data)
728
+ event.dispatcher.emit(event.hook.finished, message.data)
699
729
  break
700
730
  }
701
731
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-rc.2",
3
+ "version": "4.0.0-rc.20",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -26,7 +26,8 @@
26
26
  "lib",
27
27
  "translations",
28
28
  "typings/**/*.d.ts",
29
- "docs/webapi/**"
29
+ "docs/*.md",
30
+ "docs/helpers/**"
30
31
  ],
31
32
  "main": "lib/index.js",
32
33
  "module": "lib/index.js",
@@ -41,12 +42,15 @@
41
42
  "./els": "./lib/els.js",
42
43
  "./effects": "./lib/effects.js",
43
44
  "./steps": "./lib/steps.js",
44
- "./store": "./lib/store.js"
45
+ "./store": "./lib/store.js",
46
+ "./assertions": "./lib/assertions.js"
45
47
  },
46
48
  "bin": {
47
- "codeceptjs": "./bin/codecept.js"
49
+ "codeceptjs": "./bin/codecept.js",
50
+ "codeceptjs-mcp": "./bin/mcp-server.js",
51
+ "codeceptq": "./bin/codeceptq.js"
48
52
  },
49
- "repository": "Codeception/codeceptjs",
53
+ "repository": "codeceptjs/CodeceptJS",
50
54
  "scripts": {
51
55
  "test-server": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010 --read-only",
52
56
  "test-server:writable": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010",
@@ -85,11 +89,12 @@
85
89
  "publish-beta": "./runok.cjs publish:next-beta-version"
86
90
  },
87
91
  "dependencies": {
88
- "@codeceptjs/configure": "1.0.6",
92
+ "@codeceptjs/configure": "^4.0.0-beta.4",
89
93
  "@codeceptjs/helper": "2.0.4",
90
94
  "@cucumber/cucumber-expressions": "18",
91
95
  "@cucumber/gherkin": "38.0.0",
92
96
  "@cucumber/messages": "32.0.1",
97
+ "@modelcontextprotocol/sdk": "^1.26.0",
93
98
  "@xmldom/xmldom": "0.9.8",
94
99
  "acorn": "8.15.0",
95
100
  "ai": "^6.0.43",
@@ -112,7 +117,6 @@
112
117
  "html-minifier-terser": "7.2.0",
113
118
  "inquirer": "^8.2.7",
114
119
  "invisi-data": "^1.0.0",
115
- "joi": "18.0.2",
116
120
  "js-beautify": "1.15.4",
117
121
  "lodash.clonedeep": "4.5.0",
118
122
  "lodash.merge": "4.6.2",
@@ -128,14 +132,16 @@
128
132
  "promise-retry": "1.1.1",
129
133
  "resq": "1.11.0",
130
134
  "sprintf-js": "1.1.3",
131
- "uuid": "11.1.0"
135
+ "uuid": "11.1.0",
136
+ "xpath": "0.0.34",
137
+ "zod": "^4.1.11"
132
138
  },
133
139
  "optionalDependencies": {
134
140
  "@codeceptjs/detox-helper": "1.1.13"
135
141
  },
136
142
  "devDependencies": {
137
143
  "@apollo/server": "^5",
138
- "@codeceptjs/expect-helper": "^1.0.2",
144
+ "@codeceptjs/expect-helper": "^4.0.0-beta.5",
139
145
  "@codeceptjs/mock-request": "0.3.1",
140
146
  "@eslint/eslintrc": "3.3.3",
141
147
  "@eslint/js": "9.39.2",
@@ -170,7 +176,7 @@
170
176
  "jsdoc-typeof-plugin": "1.0.0",
171
177
  "json-server": "0.17.4",
172
178
  "mochawesome": "^7.1.3",
173
- "playwright": "1.55.1",
179
+ "playwright": "^1.59.0",
174
180
  "prettier": "^3.3.2",
175
181
  "puppeteer": "24.36.0",
176
182
  "qrcode-terminal": "0.12.0",
@@ -189,8 +195,7 @@
189
195
  "typescript": "5.9.3",
190
196
  "wdio-docker-service": "3.2.1",
191
197
  "webdriverio": "9.23.0",
192
- "xml2js": "0.6.2",
193
- "xpath": "0.0.34"
198
+ "xml2js": "0.6.2"
194
199
  },
195
200
  "peerDependencies": {
196
201
  "tsx": "^4.0.0"
@@ -212,6 +217,7 @@
212
217
  }
213
218
  },
214
219
  "overrides": {
215
- "tmp": "0.2.5"
220
+ "tmp": "0.2.5",
221
+ "js-yaml": "^4.1.1"
216
222
  }
217
223
  }