codeceptjs 4.0.1-beta.9 → 4.0.1

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 (328) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +17 -4
  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 +14 -10
  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 -2
  112. package/lib/command/run-workers.js +14 -16
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +117 -9
  116. package/lib/config.js +98 -19
  117. package/lib/container.js +188 -19
  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 +367 -516
  128. package/lib/helper/Puppeteer.js +343 -197
  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 +6 -15
  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 +13 -28
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/test.js +4 -2
  155. package/lib/mocha/ui.js +5 -6
  156. package/lib/output.js +2 -2
  157. package/lib/parser.js +2 -2
  158. package/lib/pause.js +38 -4
  159. package/lib/plugin/aiTrace.js +457 -0
  160. package/lib/plugin/analyze.js +9 -9
  161. package/lib/plugin/auth.js +5 -4
  162. package/lib/plugin/browser.js +77 -0
  163. package/lib/plugin/expose.js +159 -0
  164. package/lib/plugin/heal.js +47 -3
  165. package/lib/plugin/junitReporter.js +303 -0
  166. package/lib/plugin/pageInfo.js +54 -52
  167. package/lib/plugin/pause.js +131 -0
  168. package/lib/plugin/pauseOnFail.js +11 -33
  169. package/lib/plugin/retryFailedStep.js +43 -32
  170. package/lib/plugin/screencast.js +289 -0
  171. package/lib/plugin/screenshot.js +558 -0
  172. package/lib/plugin/screenshotOnFail.js +9 -170
  173. package/lib/plugin/stepTimeout.js +3 -2
  174. package/lib/recorder.js +1 -1
  175. package/lib/rerun.js +2 -1
  176. package/lib/result.js +2 -1
  177. package/lib/step/base.js +23 -9
  178. package/lib/step/comment.js +2 -2
  179. package/lib/step/config.js +15 -2
  180. package/lib/step/helper.js +4 -4
  181. package/lib/step/meta.js +3 -3
  182. package/lib/step/record.js +12 -4
  183. package/lib/store.js +72 -3
  184. package/lib/translation.js +2 -1
  185. package/lib/utils/loaderCheck.js +41 -3
  186. package/lib/utils/mask_data.js +2 -1
  187. package/lib/utils/pluginParser.js +151 -0
  188. package/lib/utils/trace.js +297 -0
  189. package/lib/utils/typescript.js +261 -49
  190. package/lib/utils.js +77 -3
  191. package/lib/workers.js +123 -17
  192. package/package.json +48 -43
  193. package/typings/index.d.ts +120 -9
  194. package/typings/promiseBasedTypes.d.ts +3243 -6057
  195. package/typings/types.d.ts +3541 -6506
  196. package/docs/webapi/amOnPage.mustache +0 -11
  197. package/docs/webapi/appendField.mustache +0 -11
  198. package/docs/webapi/attachFile.mustache +0 -12
  199. package/docs/webapi/blur.mustache +0 -18
  200. package/docs/webapi/checkOption.mustache +0 -13
  201. package/docs/webapi/clearCookie.mustache +0 -9
  202. package/docs/webapi/clearField.mustache +0 -9
  203. package/docs/webapi/click.mustache +0 -29
  204. package/docs/webapi/clickLink.mustache +0 -8
  205. package/docs/webapi/closeCurrentTab.mustache +0 -7
  206. package/docs/webapi/closeOtherTabs.mustache +0 -8
  207. package/docs/webapi/dontSee.mustache +0 -11
  208. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  209. package/docs/webapi/dontSeeCookie.mustache +0 -8
  210. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  211. package/docs/webapi/dontSeeElement.mustache +0 -8
  212. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  213. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  214. package/docs/webapi/dontSeeInField.mustache +0 -11
  215. package/docs/webapi/dontSeeInSource.mustache +0 -8
  216. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  217. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  218. package/docs/webapi/doubleClick.mustache +0 -13
  219. package/docs/webapi/downloadFile.mustache +0 -12
  220. package/docs/webapi/dragAndDrop.mustache +0 -9
  221. package/docs/webapi/dragSlider.mustache +0 -11
  222. package/docs/webapi/executeAsyncScript.mustache +0 -24
  223. package/docs/webapi/executeScript.mustache +0 -26
  224. package/docs/webapi/fillField.mustache +0 -16
  225. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  226. package/docs/webapi/focus.mustache +0 -13
  227. package/docs/webapi/forceClick.mustache +0 -28
  228. package/docs/webapi/forceRightClick.mustache +0 -18
  229. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  230. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  231. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  232. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  233. package/docs/webapi/grabCookie.mustache +0 -11
  234. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  235. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  236. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  237. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  238. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  239. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  240. package/docs/webapi/grabGeoLocation.mustache +0 -8
  241. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  242. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  243. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  244. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  245. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  246. package/docs/webapi/grabPopupText.mustache +0 -5
  247. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  248. package/docs/webapi/grabSource.mustache +0 -8
  249. package/docs/webapi/grabTextFrom.mustache +0 -10
  250. package/docs/webapi/grabTextFromAll.mustache +0 -9
  251. package/docs/webapi/grabTitle.mustache +0 -8
  252. package/docs/webapi/grabValueFrom.mustache +0 -9
  253. package/docs/webapi/grabValueFromAll.mustache +0 -8
  254. package/docs/webapi/grabWebElement.mustache +0 -9
  255. package/docs/webapi/grabWebElements.mustache +0 -9
  256. package/docs/webapi/moveCursorTo.mustache +0 -12
  257. package/docs/webapi/openNewTab.mustache +0 -7
  258. package/docs/webapi/pressKey.mustache +0 -12
  259. package/docs/webapi/pressKeyDown.mustache +0 -12
  260. package/docs/webapi/pressKeyUp.mustache +0 -12
  261. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  262. package/docs/webapi/refreshPage.mustache +0 -6
  263. package/docs/webapi/resizeWindow.mustache +0 -6
  264. package/docs/webapi/rightClick.mustache +0 -14
  265. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  266. package/docs/webapi/saveScreenshot.mustache +0 -12
  267. package/docs/webapi/say.mustache +0 -10
  268. package/docs/webapi/scrollIntoView.mustache +0 -11
  269. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  270. package/docs/webapi/scrollPageToTop.mustache +0 -6
  271. package/docs/webapi/scrollTo.mustache +0 -12
  272. package/docs/webapi/see.mustache +0 -11
  273. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  274. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  275. package/docs/webapi/seeCookie.mustache +0 -8
  276. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  277. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  278. package/docs/webapi/seeElement.mustache +0 -8
  279. package/docs/webapi/seeElementInDOM.mustache +0 -8
  280. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  281. package/docs/webapi/seeInField.mustache +0 -12
  282. package/docs/webapi/seeInPopup.mustache +0 -8
  283. package/docs/webapi/seeInSource.mustache +0 -7
  284. package/docs/webapi/seeInTitle.mustache +0 -8
  285. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  286. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  287. package/docs/webapi/seeTextEquals.mustache +0 -9
  288. package/docs/webapi/seeTitleEquals.mustache +0 -8
  289. package/docs/webapi/seeTraffic.mustache +0 -36
  290. package/docs/webapi/selectOption.mustache +0 -21
  291. package/docs/webapi/setCookie.mustache +0 -16
  292. package/docs/webapi/setGeoLocation.mustache +0 -12
  293. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  294. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  295. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  296. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  297. package/docs/webapi/switchTo.mustache +0 -9
  298. package/docs/webapi/switchToNextTab.mustache +0 -10
  299. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  300. package/docs/webapi/type.mustache +0 -21
  301. package/docs/webapi/uncheckOption.mustache +0 -13
  302. package/docs/webapi/wait.mustache +0 -8
  303. package/docs/webapi/waitForClickable.mustache +0 -11
  304. package/docs/webapi/waitForCookie.mustache +0 -9
  305. package/docs/webapi/waitForDetached.mustache +0 -10
  306. package/docs/webapi/waitForDisabled.mustache +0 -6
  307. package/docs/webapi/waitForElement.mustache +0 -11
  308. package/docs/webapi/waitForEnabled.mustache +0 -6
  309. package/docs/webapi/waitForFunction.mustache +0 -17
  310. package/docs/webapi/waitForInvisible.mustache +0 -10
  311. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  312. package/docs/webapi/waitForText.mustache +0 -13
  313. package/docs/webapi/waitForValue.mustache +0 -10
  314. package/docs/webapi/waitForVisible.mustache +0 -10
  315. package/docs/webapi/waitInUrl.mustache +0 -9
  316. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  317. package/docs/webapi/waitToHide.mustache +0 -10
  318. package/docs/webapi/waitUrlEquals.mustache +0 -10
  319. package/lib/helper/AI.js +0 -214
  320. package/lib/helper/Mochawesome.js +0 -96
  321. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  322. package/lib/helper/extras/React.js +0 -65
  323. package/lib/listener/enhancedGlobalRetry.js +0 -110
  324. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  325. package/lib/plugin/htmlReporter.js +0 -3648
  326. package/lib/plugin/stepByStepReport.js +0 -427
  327. package/lib/plugin/subtitles.js +0 -89
  328. package/lib/retryCoordinator.js +0 -207
@@ -0,0 +1,457 @@
1
+ import fs from 'fs'
2
+ import { mkdirp } from 'mkdirp'
3
+ import path from 'path'
4
+
5
+ import store from '../store.js'
6
+ import Container from '../container.js'
7
+ import recorder from '../recorder.js'
8
+ import event from '../event.js'
9
+ import output from '../output.js'
10
+ import { deleteDir, clearString } from '../utils.js'
11
+ import { captureSnapshot, pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
12
+ import {
13
+ parsePluginArgs,
14
+ resolveTrigger,
15
+ matchStepFile,
16
+ matchUrl,
17
+ } from '../utils/pluginParser.js'
18
+
19
+ const defaultConfig = {
20
+ on: 'step',
21
+ deleteSuccessful: false,
22
+ fullPageScreenshots: false,
23
+ output: store.outputDir,
24
+ captureHTML: true,
25
+ captureARIA: true,
26
+ captureBrowserLogs: true,
27
+ captureHTTP: true,
28
+ captureDebugOutput: true,
29
+ ignoreSteps: [],
30
+ }
31
+
32
+ /**
33
+ *
34
+ * Generates AI-friendly trace files for debugging with AI agents.
35
+ * This plugin creates a markdown file with test execution logs and links to all artifacts
36
+ * (screenshots, HTML, ARIA snapshots, browser logs, HTTP requests) for each step.
37
+ *
38
+ * #### Configuration
39
+ *
40
+ * ```js
41
+ * "plugins": {
42
+ * "aiTrace": {
43
+ * "enabled": true
44
+ * }
45
+ * }
46
+ * ```
47
+ *
48
+ * Possible config options:
49
+ *
50
+ * * `deleteSuccessful`: delete traces for successfully executed tests. Default: false.
51
+ * * `fullPageScreenshots`: should full page screenshots be used. Default: false.
52
+ * * `output`: a directory where traces should be stored. Default: `output`.
53
+ * * `captureHTML`: capture HTML for each step. Default: true.
54
+ * * `captureARIA`: capture ARIA snapshot for each step. Default: true.
55
+ * * `captureBrowserLogs`: capture browser console logs. Default: true.
56
+ * * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
57
+ * * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
58
+ * * `ignoreSteps`: steps to ignore in trace. Array of RegExps is expected.
59
+ * * `on`: trigger mode — `step` (default), `fail`, `test`, `file`, `url`.
60
+ *
61
+ * #### `on=` modes
62
+ *
63
+ * * **step** — persist every step (default)
64
+ * * **fail** — persist only the failed step
65
+ * * **test** — persist only the last step of each test
66
+ * * **file** — persist steps from `path=...[;line=...]`
67
+ * * **url** — persist when the current URL matches `pattern=...`
68
+ *
69
+ * @param {*} config
70
+ */
71
+ export default function (config = {}) {
72
+ const cliArgs = parsePluginArgs(config._args)
73
+ const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'aiTrace' })
74
+ if (!trigger) return
75
+
76
+ config = Object.assign(defaultConfig, config)
77
+
78
+ const helper = pickActingHelper(Container.helpers())
79
+
80
+ if (!helper) {
81
+ output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
82
+ return
83
+ }
84
+
85
+ let dir
86
+ let stepNum
87
+ let steps = []
88
+ let debugOutput = []
89
+ let error
90
+ let savedSteps = new Set()
91
+ let currentTest = null
92
+ let testStartTime
93
+ let currentUrl = null
94
+ let testFailed = false
95
+ let pendingArtifactCapture = null
96
+ let firstFailedStepSaved = false
97
+
98
+ const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
99
+
100
+ if (config.captureDebugOutput) {
101
+ const originalDebug = output.debug
102
+ output.debug = function (...args) {
103
+ debugOutput.push(args.join(' '))
104
+ originalDebug.apply(output, args)
105
+ }
106
+ }
107
+
108
+ event.dispatcher.on(event.suite.before, suite => {
109
+ stepNum = -1
110
+ })
111
+
112
+ event.dispatcher.on(event.test.before, test => {
113
+ let title
114
+ try {
115
+ title = test.fullTitle ? test.fullTitle() : test.title
116
+ } catch (err) {
117
+ title = test.title
118
+ }
119
+ dir = traceDirFor(test.file, title, reportDir)
120
+ mkdirp.sync(dir)
121
+ deleteDir(dir)
122
+ mkdirp.sync(dir)
123
+ stepNum = 0
124
+ error = null
125
+ steps = []
126
+ debugOutput = []
127
+ savedSteps.clear()
128
+ currentTest = test
129
+ testStartTime = Date.now()
130
+ currentUrl = null
131
+ testFailed = false
132
+ firstFailedStepSaved = false
133
+ pendingArtifactCapture = null
134
+ })
135
+
136
+ event.dispatcher.on(event.step.after, step => {
137
+ if (!currentTest) return
138
+ if (step.status === 'failed') {
139
+ testFailed = true
140
+ }
141
+ if (step.status === 'queued' && testFailed) {
142
+ output.debug(`aiTrace: Skipping queued step "${step.toString()}" - testFailed: ${testFailed}`)
143
+ return
144
+ }
145
+ if (step.status === 'failed' && firstFailedStepSaved) {
146
+ output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
147
+ return
148
+ }
149
+
150
+ // on= filtering
151
+ if (trigger.on === 'fail') return // failed steps handled by step.failed
152
+ if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
153
+ if (trigger.on === 'url') {
154
+ recorder.add('aiTrace:url check', async () => {
155
+ try {
156
+ if (!helper.grabCurrentUrl) return
157
+ const url = await helper.grabCurrentUrl()
158
+ if (!matchUrl(url, trigger.pattern)) return
159
+ await persistStep(step)
160
+ } catch (err) {
161
+ output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
162
+ }
163
+ }, true)
164
+ return
165
+ }
166
+
167
+ recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
168
+ output.debug(`aiTrace: Error saving step: ${err.message}`)
169
+ }), true)
170
+ })
171
+
172
+ event.dispatcher.on(event.step.failed, step => {
173
+ if (!currentTest) return
174
+ if (step.status === 'queued' && testFailed) {
175
+ output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
176
+ return
177
+ }
178
+ if (firstFailedStepSaved) {
179
+ output.debug(`aiTrace: Skipping subsequent failed step "${step.toString()}" - already saved first failed step`)
180
+ return
181
+ }
182
+
183
+ const stepKey = step.toString()
184
+ if (savedSteps.has(stepKey)) {
185
+ const existingStep = steps.find(s => s.step === stepKey)
186
+ if (!existingStep) {
187
+ output.debug(`aiTrace: Step "${stepKey}" marked as saved but not found in steps array`)
188
+ return
189
+ }
190
+ existingStep.status = 'failed'
191
+
192
+ pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
193
+ output.debug(`aiTrace: Error updating failed step: ${err.message}`)
194
+ })
195
+ } else {
196
+ if (stepNum === -1) return
197
+ if (isStepIgnored(step)) return
198
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
199
+
200
+ const stepPrefix = generateStepPrefix(step, stepNum)
201
+ stepNum++
202
+
203
+ const stepData = {
204
+ step: stepKey,
205
+ status: 'failed',
206
+ prefix: stepPrefix,
207
+ artifacts: {},
208
+ meta: {},
209
+ debugOutput: [],
210
+ }
211
+
212
+ if (step.startTime && step.endTime) {
213
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
214
+ }
215
+
216
+ savedSteps.add(stepKey)
217
+ steps.push(stepData)
218
+ firstFailedStepSaved = true
219
+
220
+ pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
221
+ output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
222
+ })
223
+ }
224
+ })
225
+
226
+ event.dispatcher.on(event.test.passed, test => {
227
+ if (config.deleteSuccessful) {
228
+ deleteDir(dir)
229
+ return
230
+ }
231
+ persist(test, 'passed')
232
+ })
233
+
234
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
235
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
236
+ return
237
+ }
238
+ recorder.add('aiTrace:persist failed', async () => {
239
+ if (pendingArtifactCapture) {
240
+ await pendingArtifactCapture
241
+ pendingArtifactCapture = null
242
+ }
243
+ persist(test, 'failed')
244
+ }, true)
245
+ })
246
+
247
+ async function persistStep(step) {
248
+ if (stepNum === -1) return
249
+ if (isStepIgnored(step)) return
250
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
251
+
252
+ const stepKey = step.toString()
253
+
254
+ if (savedSteps.has(stepKey)) {
255
+ const existingStep = steps.find(s => s.step === stepKey)
256
+ if (existingStep && step.status === 'failed') {
257
+ existingStep.status = 'failed'
258
+ step.artifacts = {}
259
+ await captureArtifactsForStep(step, existingStep, existingStep.prefix)
260
+ }
261
+ return
262
+ }
263
+ savedSteps.add(stepKey)
264
+
265
+ const stepPrefix = generateStepPrefix(step, stepNum)
266
+ stepNum++
267
+
268
+ const stepData = {
269
+ step: step.toString(),
270
+ status: step.status,
271
+ prefix: stepPrefix,
272
+ artifacts: {},
273
+ meta: {},
274
+ debugOutput: [],
275
+ }
276
+
277
+ if (step.startTime && step.endTime) {
278
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
279
+ }
280
+
281
+ if (config.captureDebugOutput && debugOutput.length > 0) {
282
+ stepData.debugOutput = [...debugOutput]
283
+ debugOutput = []
284
+ }
285
+
286
+ await captureArtifactsForStep(step, stepData, stepPrefix)
287
+ steps.push(stepData)
288
+ }
289
+
290
+ async function captureArtifactsForStep(step, stepData, stepPrefix) {
291
+ if (!step.artifacts) {
292
+ step.artifacts = {}
293
+ }
294
+
295
+ let browserAvailable = true
296
+
297
+ try {
298
+ try {
299
+ if (helper.grabCurrentUrl) {
300
+ const url = await helper.grabCurrentUrl()
301
+ stepData.meta.url = url
302
+ currentUrl = url
303
+ }
304
+ } catch (err) {
305
+ browserAvailable = false
306
+ output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
307
+ }
308
+
309
+ let preExistingScreenshot = false
310
+ if (step.artifacts?.screenshot) {
311
+ const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
312
+ ? step.artifacts.screenshot
313
+ : path.resolve(dir, step.artifacts.screenshot)
314
+ const screenshotFile = path.basename(screenshotPath)
315
+ stepData.artifacts.screenshot = screenshotFile
316
+ step.artifacts.screenshot = screenshotPath
317
+ preExistingScreenshot = true
318
+
319
+ if (!fs.existsSync(screenshotPath)) {
320
+ try {
321
+ await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
322
+ } catch (err) {
323
+ output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
324
+ }
325
+ }
326
+ }
327
+
328
+ const captured = await captureSnapshot(helper, {
329
+ dir,
330
+ prefix: stepPrefix,
331
+ fullPage: config.fullPageScreenshots,
332
+ captureHTML: config.captureHTML && browserAvailable,
333
+ captureARIA: config.captureARIA && browserAvailable,
334
+ captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
335
+ captureStorage: false,
336
+ })
337
+
338
+ if (!preExistingScreenshot && captured.screenshot) {
339
+ stepData.artifacts.screenshot = captured.screenshot
340
+ step.artifacts.screenshot = path.join(dir, captured.screenshot)
341
+ }
342
+ if (step.artifacts?.html) {
343
+ stepData.artifacts.html = step.artifacts.html
344
+ } else if (captured.html) {
345
+ stepData.artifacts.html = captured.html
346
+ }
347
+ if (captured.aria) stepData.artifacts.aria = captured.aria
348
+ if (captured.console) {
349
+ stepData.artifacts.console = captured.console
350
+ stepData.meta.consoleCount = captured.consoleCount
351
+ }
352
+ } catch (err) {
353
+ output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
354
+ }
355
+ }
356
+
357
+ function persist(test, status) {
358
+ if (!steps.length) {
359
+ output.debug('aiTrace: No steps to save in trace')
360
+ return
361
+ }
362
+
363
+ // on=test: only render the last step in markdown; artifacts of earlier steps
364
+ // remain on disk unreferenced.
365
+ if (trigger.on === 'test') {
366
+ steps = steps.slice(-1)
367
+ }
368
+
369
+ const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
370
+
371
+ let markdown = `file: ${test.file || 'unknown'}\n`
372
+ markdown += `name: ${test.title}\n`
373
+ markdown += `time: ${testDuration}s\n`
374
+ markdown += `---\n\n`
375
+
376
+ if (status === 'failed') {
377
+ if (test.art && test.art.message) {
378
+ markdown += `Error: ${test.art.message}\n\n`
379
+ }
380
+ if (test.art && test.art.stack) {
381
+ markdown += `${test.art.stack}\n\n`
382
+ }
383
+ markdown += `---\n\n`
384
+ }
385
+
386
+ if (config.captureDebugOutput && debugOutput.length > 0) {
387
+ markdown += `CodeceptJS Debug Output:\n\n`
388
+ debugOutput.forEach(line => {
389
+ markdown += `> ${line}\n`
390
+ })
391
+ markdown += `\n---\n\n`
392
+ }
393
+
394
+ steps.forEach((stepData, index) => {
395
+ const stepAnchor = clearString(stepData.step).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)
396
+ markdown += `### Step ${index + 1}: ${stepData.step}\n`
397
+ markdown += `<a id="${stepAnchor}"></a>\n`
398
+
399
+ if (stepData.meta.duration) {
400
+ markdown += ` > duration: ${stepData.meta.duration}\n`
401
+ }
402
+
403
+ if (stepData.meta.url) {
404
+ markdown += ` > navigated to ${stepData.meta.url}\n`
405
+ }
406
+
407
+ if (config.captureDebugOutput && stepData.debugOutput && stepData.debugOutput.length > 0) {
408
+ stepData.debugOutput.forEach(line => {
409
+ markdown += ` > ${line}\n`
410
+ })
411
+ }
412
+
413
+ const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
414
+ if (links) markdown += links + '\n'
415
+
416
+ if (config.captureHTTP) {
417
+ if (test.artifacts && test.artifacts.har) {
418
+ const harPath = path.relative(reportDir, test.artifacts.har)
419
+ markdown += ` > HTTP: see [HAR file](../${harPath}) for network requests\n`
420
+ } else if (test.artifacts && test.artifacts.trace) {
421
+ const tracePath = path.relative(reportDir, test.artifacts.trace)
422
+ markdown += ` > HTTP: see [Playwright trace](../${tracePath}) for network requests\n`
423
+ }
424
+ }
425
+
426
+ markdown += `\n`
427
+ })
428
+
429
+ const traceFile = path.join(dir, 'trace.md')
430
+ fs.writeFileSync(traceFile, markdown)
431
+
432
+ output.print(`Trace Saved: file://${traceFile}`)
433
+
434
+ if (!test.artifacts) test.artifacts = {}
435
+ test.artifacts.aiTrace = traceFile
436
+ }
437
+
438
+ function isStepIgnored(step) {
439
+ if (!config.ignoreSteps) return false
440
+ if (!step.title) return false
441
+ for (const pattern of config.ignoreSteps || []) {
442
+ if (step.title.match(pattern)) return true
443
+ }
444
+ return false
445
+ }
446
+
447
+ function generateStepPrefix(step, index) {
448
+ const stepName = step.toString()
449
+ const cleanedName = clearString(stepName)
450
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
451
+ .replace(/_{2,}/g, '_')
452
+ .slice(0, 80)
453
+ .trim()
454
+
455
+ return `${String(index).padStart(4, '0')}_${cleanedName}`
456
+ }
457
+ }
@@ -12,6 +12,7 @@ const ai = aiModule.default || aiModule
12
12
  import colors from 'chalk'
13
13
  import ora from 'ora'
14
14
  import event from '../event.js'
15
+ import recorder from '../recorder.js'
15
16
 
16
17
  import output from '../output.js'
17
18
 
@@ -154,10 +155,9 @@ const defaultConfig = {
154
155
  if (config.vision && test.artifacts.screenshot) {
155
156
  debug('Adding screenshot to prompt')
156
157
  messages[0].content.push({
157
- type: 'image_url',
158
- image_url: {
159
- url: 'data:image/png;base64,' + base64EncodeFile(test.artifacts.screenshot),
160
- },
158
+ type: 'image',
159
+ image: base64EncodeFile(test.artifacts.screenshot),
160
+ mediaType: 'image/png',
161
161
  })
162
162
  }
163
163
 
@@ -227,14 +227,14 @@ export default function (config = {}) {
227
227
  console.log('Enabled AI analysis')
228
228
  })
229
229
 
230
- event.dispatcher.on(event.all.result, async result => {
230
+ event.dispatcher.on(event.all.result, result => {
231
231
  if (!isMainThread) return // run only on main thread
232
232
  if (!ai.isEnabled) {
233
233
  console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
234
234
  return
235
235
  }
236
236
 
237
- printReport(result)
237
+ recorder.add('analyze:print-ai-report', () => printReport(result), true)
238
238
  })
239
239
 
240
240
  event.dispatcher.on(event.workers.result, async result => {
@@ -248,7 +248,7 @@ export default function (config = {}) {
248
248
  return
249
249
  }
250
250
 
251
- printReport(result)
251
+ await printReport(result)
252
252
  })
253
253
 
254
254
  async function printReport(result) {
@@ -294,7 +294,7 @@ export default function (config = {}) {
294
294
  console.error('Error analyzing failed tests', err)
295
295
  }
296
296
 
297
- if (!Object.keys(container.plugins()).includes('pageInfo')) {
297
+ if (!Object.keys(Container.plugins()).includes('pageInfo')) {
298
298
  console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
299
299
  }
300
300
  }
@@ -353,7 +353,7 @@ function serializeError(error) {
353
353
  errorMessage +=
354
354
  '\n' +
355
355
  error.stack
356
- .replace(global.codecept_dir || '', '.')
356
+ .replace(store.codeceptDir || '', '.')
357
357
  .split('\n')
358
358
  .map(line => line.replace(ansiRegExp(), ''))
359
359
  .slice(0, 5)
@@ -321,7 +321,7 @@ export default function (config) {
321
321
  }
322
322
  if (config.saveToFile) {
323
323
  output.debug(`Saved user session into file for ${name}`)
324
- fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies))
324
+ fs.writeFileSync(path.join(store.outputDir, `${name}_session.json`), JSON.stringify(cookies))
325
325
  }
326
326
  store[`${name}_session`] = cookies
327
327
  }
@@ -350,7 +350,8 @@ export default function (config) {
350
350
  recorder.session.restore('auto login')
351
351
  recorder.session.restore('check login')
352
352
  section.end()
353
- recorder.throw(err)
353
+ // Use regular throw instead of recorder.throw to avoid promise chaining cycle
354
+ throw err
354
355
  })
355
356
  })
356
357
  recorder.add(() => {
@@ -376,7 +377,7 @@ export default function (config) {
376
377
  }
377
378
 
378
379
  if (!config.saveToFile) return
379
- const cookieFile = path.join(global.output_dir, `${name}_session.json`)
380
+ const cookieFile = path.join(store.outputDir, `${name}_session.json`)
380
381
 
381
382
  if (!fileExists(cookieFile)) {
382
383
  return
@@ -411,7 +412,7 @@ export default function (config) {
411
412
 
412
413
  function loadCookiesFromFile(config) {
413
414
  for (const name in config.users) {
414
- const fileName = path.join(global.output_dir, `${name}_session.json`)
415
+ const fileName = path.join(store.outputDir, `${name}_session.json`)
415
416
  if (!fileExists(fileName)) continue
416
417
  const data = fs.readFileSync(fileName).toString()
417
418
  try {
@@ -0,0 +1,77 @@
1
+ import output from '../output.js'
2
+
3
+ /**
4
+ * Overrides browser helper config from the command line. Works for all browser helpers
5
+ * (Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
6
+ *
7
+ * Enable it via `-p` option with one or more colon-chained args:
8
+ *
9
+ * ```
10
+ * npx codeceptjs run -p browser:show
11
+ * npx codeceptjs run -p browser:hide
12
+ * npx codeceptjs run -p browser:browser=firefox
13
+ * npx codeceptjs run -p browser:windowSize=1024x768:video=false
14
+ * npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
15
+ * ```
16
+ *
17
+ * #### Args
18
+ *
19
+ * * **show** — force visible browser
20
+ * * **hide** — force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
21
+ * * **`<key>=<value>`** — set `helpers.<eachBrowserHelper>.<key> = <value>`. Three keys
22
+ * get per-helper translation via `setBrowserConfig`:
23
+ * * `browser=<name>` — Puppeteer receives `product`, Playwright receives `browser`
24
+ * * `windowSize=WxH` — also adds `--window-size=W,H` chromium/chrome args
25
+ * * `show=true|false` — toggles `show` on Playwright/Puppeteer; injects/strips
26
+ * `--headless` in WebDriver chrome/firefox capability args
27
+ *
28
+ * Values stay as strings. `true` / `false` are coerced to booleans.
29
+ *
30
+ * Requires `@codeceptjs/configure` to be installed; if missing, the plugin
31
+ * logs a hint and skips the override.
32
+ */
33
+ export default async function (config = {}) {
34
+ const { _args, enabled, ...rest } = config
35
+ const opts = { ...rest, ...parseArgs(_args || []) }
36
+ if (Object.keys(opts).length === 0) return
37
+
38
+ const configure = await tryImportConfigure()
39
+ if (!configure) return
40
+
41
+ configure.setBrowserConfig(opts)
42
+ output.debug(`browser plugin: applied ${formatOpts(opts)}`)
43
+ }
44
+
45
+ async function tryImportConfigure() {
46
+ try {
47
+ return await import('@codeceptjs/configure')
48
+ } catch (err) {
49
+ output.error("browser plugin: '@codeceptjs/configure' is not installed; CLI overrides are skipped. Run `npm i @codeceptjs/configure` to enable.")
50
+ return null
51
+ }
52
+ }
53
+
54
+ function parseArgs(args) {
55
+ return args.filter(Boolean).reduce((acc, arg) => Object.assign(acc, parseArg(arg)), {})
56
+ }
57
+
58
+ function parseArg(arg) {
59
+ if (arg === 'show') return { show: true }
60
+ if (arg === 'hide') return { show: false }
61
+ if (arg.includes('=')) {
62
+ const [key, ...rest] = arg.split('=')
63
+ return { [key]: parseValue(rest.join('=')) }
64
+ }
65
+ output.error(`browser plugin: unknown arg "${arg}"`)
66
+ return {}
67
+ }
68
+
69
+ function parseValue(v) {
70
+ if (v === 'true') return true
71
+ if (v === 'false') return false
72
+ return v
73
+ }
74
+
75
+ function formatOpts(opts) {
76
+ return Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
77
+ }