codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21

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 (296) 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 +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -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/environment-variables.md +131 -0
  30. package/docs/examples.md +161 -0
  31. package/docs/heal.md +213 -0
  32. package/docs/helpers/ApiDataFactory.md +267 -0
  33. package/docs/helpers/Appium.md +1405 -0
  34. package/docs/helpers/Detox.md +665 -0
  35. package/docs/helpers/ExpectHelper.md +275 -0
  36. package/docs/helpers/FileSystem.md +152 -0
  37. package/docs/helpers/GraphQL.md +152 -0
  38. package/docs/helpers/GraphQLDataFactory.md +226 -0
  39. package/docs/helpers/JSONResponse.md +255 -0
  40. package/docs/helpers/Mochawesome.md +8 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/MockServer.md +212 -0
  43. package/docs/helpers/Playwright.md +2969 -0
  44. package/docs/helpers/Polly.md +44 -0
  45. package/docs/helpers/Protractor.md +1769 -0
  46. package/docs/helpers/Puppeteer-firefox.md +86 -0
  47. package/docs/helpers/Puppeteer.md +2690 -0
  48. package/docs/helpers/REST.md +289 -0
  49. package/docs/helpers/SoftExpectHelper.md +352 -0
  50. package/docs/helpers/WebDriver.md +2682 -0
  51. package/docs/hooks.md +339 -0
  52. package/docs/index.md +111 -0
  53. package/docs/installation.md +83 -0
  54. package/docs/internal-api.md +265 -0
  55. package/docs/internal-test-server.md +89 -0
  56. package/docs/locators.md +355 -0
  57. package/docs/mcp.md +485 -0
  58. package/docs/migration-4.md +556 -0
  59. package/docs/mobile.md +338 -0
  60. package/docs/pageobjects.md +399 -0
  61. package/docs/parallel.md +585 -0
  62. package/docs/playwright.md +714 -0
  63. package/docs/plugins.md +866 -0
  64. package/docs/puppeteer.md +314 -0
  65. package/docs/quickstart.md +120 -0
  66. package/docs/react.md +70 -0
  67. package/docs/reports.md +483 -0
  68. package/docs/retry.md +274 -0
  69. package/docs/secrets.md +150 -0
  70. package/docs/sessions.md +80 -0
  71. package/docs/shadow.md +68 -0
  72. package/docs/test-structure.md +275 -0
  73. package/docs/timeouts.md +183 -0
  74. package/docs/translation.md +247 -0
  75. package/docs/tutorial.md +271 -0
  76. package/docs/typescript.md +374 -0
  77. package/docs/web-element.md +251 -0
  78. package/docs/webdriver.md +708 -0
  79. package/docs/within.md +55 -0
  80. package/lib/ai.js +3 -2
  81. package/lib/aria.js +260 -0
  82. package/lib/assertions.js +18 -0
  83. package/lib/codecept.js +27 -24
  84. package/lib/command/check.js +2 -1
  85. package/lib/command/dryRun.js +24 -5
  86. package/lib/command/generate.js +2 -0
  87. package/lib/command/gherkin/snippets.js +5 -4
  88. package/lib/command/init.js +248 -269
  89. package/lib/command/list.js +150 -10
  90. package/lib/command/query.js +218 -0
  91. package/lib/command/run-multiple.js +2 -0
  92. package/lib/command/run-workers.js +2 -14
  93. package/lib/command/run.js +3 -17
  94. package/lib/command/utils.js +14 -0
  95. package/lib/command/workers/runTests.js +10 -10
  96. package/lib/config.js +77 -4
  97. package/lib/container.js +114 -17
  98. package/lib/effects.js +17 -0
  99. package/lib/element/WebElement.js +246 -2
  100. package/lib/els.js +12 -6
  101. package/lib/globals.js +32 -19
  102. package/lib/heal.js +6 -3
  103. package/lib/helper/ApiDataFactory.js +2 -1
  104. package/lib/helper/Appium.js +8 -8
  105. package/lib/helper/FileSystem.js +3 -2
  106. package/lib/helper/GraphQLDataFactory.js +2 -1
  107. package/lib/helper/Playwright.js +233 -162
  108. package/lib/helper/Puppeteer.js +208 -76
  109. package/lib/helper/WebDriver.js +173 -68
  110. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  111. package/lib/helper/errors/NonFocusedType.js +8 -0
  112. package/lib/helper/extras/Download.js +45 -0
  113. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  114. package/lib/helper/extras/elementSelection.js +58 -0
  115. package/lib/helper/extras/focusCheck.js +43 -0
  116. package/lib/helper/extras/richTextEditor.js +178 -0
  117. package/lib/helper/scripts/dropFile.js +11 -0
  118. package/lib/history.js +3 -2
  119. package/lib/html.js +103 -16
  120. package/lib/index.js +9 -1
  121. package/lib/listener/config.js +6 -4
  122. package/lib/listener/emptyRun.js +2 -1
  123. package/lib/listener/globalRetry.js +32 -6
  124. package/lib/listener/helpers.js +4 -1
  125. package/lib/listener/mocha.js +2 -1
  126. package/lib/listener/pageobjects.js +43 -0
  127. package/lib/listener/result.js +3 -2
  128. package/lib/locator.js +126 -3
  129. package/lib/mocha/cli.js +14 -2
  130. package/lib/mocha/factory.js +7 -2
  131. package/lib/mocha/inject.js +1 -1
  132. package/lib/mocha/scenarioConfig.js +2 -1
  133. package/lib/mocha/ui.js +5 -6
  134. package/lib/parser.js +2 -2
  135. package/lib/pause.js +38 -4
  136. package/lib/plugin/aiTrace.js +456 -0
  137. package/lib/plugin/analyze.js +6 -5
  138. package/lib/plugin/auth.js +3 -3
  139. package/lib/plugin/browser.js +77 -0
  140. package/lib/plugin/expose.js +159 -0
  141. package/lib/plugin/heal.js +47 -3
  142. package/lib/plugin/pageInfo.js +54 -52
  143. package/lib/plugin/pause.js +131 -0
  144. package/lib/plugin/pauseOnFail.js +10 -34
  145. package/lib/plugin/retryFailedStep.js +32 -22
  146. package/lib/plugin/screencast.js +289 -0
  147. package/lib/plugin/screenshot.js +563 -0
  148. package/lib/plugin/screenshotOnFail.js +8 -171
  149. package/lib/rerun.js +2 -1
  150. package/lib/result.js +2 -1
  151. package/lib/step/base.js +3 -2
  152. package/lib/step/config.js +15 -2
  153. package/lib/step/record.js +2 -2
  154. package/lib/store.js +72 -3
  155. package/lib/translation.js +2 -1
  156. package/lib/utils/mask_data.js +2 -1
  157. package/lib/utils/pluginParser.js +151 -0
  158. package/lib/utils/trace.js +297 -0
  159. package/lib/utils.js +77 -3
  160. package/lib/workers.js +63 -25
  161. package/package.json +19 -13
  162. package/typings/index.d.ts +19 -5
  163. package/docs/webapi/amOnPage.mustache +0 -11
  164. package/docs/webapi/appendField.mustache +0 -11
  165. package/docs/webapi/attachFile.mustache +0 -12
  166. package/docs/webapi/blur.mustache +0 -18
  167. package/docs/webapi/checkOption.mustache +0 -13
  168. package/docs/webapi/clearCookie.mustache +0 -9
  169. package/docs/webapi/clearField.mustache +0 -9
  170. package/docs/webapi/click.mustache +0 -29
  171. package/docs/webapi/clickLink.mustache +0 -8
  172. package/docs/webapi/closeCurrentTab.mustache +0 -7
  173. package/docs/webapi/closeOtherTabs.mustache +0 -8
  174. package/docs/webapi/dontSee.mustache +0 -11
  175. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  176. package/docs/webapi/dontSeeCookie.mustache +0 -8
  177. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  178. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  179. package/docs/webapi/dontSeeElement.mustache +0 -8
  180. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  181. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  182. package/docs/webapi/dontSeeInField.mustache +0 -11
  183. package/docs/webapi/dontSeeInSource.mustache +0 -8
  184. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  185. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  186. package/docs/webapi/doubleClick.mustache +0 -13
  187. package/docs/webapi/downloadFile.mustache +0 -12
  188. package/docs/webapi/dragAndDrop.mustache +0 -9
  189. package/docs/webapi/dragSlider.mustache +0 -11
  190. package/docs/webapi/executeAsyncScript.mustache +0 -24
  191. package/docs/webapi/executeScript.mustache +0 -26
  192. package/docs/webapi/fillField.mustache +0 -16
  193. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  194. package/docs/webapi/focus.mustache +0 -13
  195. package/docs/webapi/forceClick.mustache +0 -28
  196. package/docs/webapi/forceRightClick.mustache +0 -18
  197. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  198. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  199. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  200. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  201. package/docs/webapi/grabCookie.mustache +0 -11
  202. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  203. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  204. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  205. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  206. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  207. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  208. package/docs/webapi/grabGeoLocation.mustache +0 -8
  209. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  210. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  211. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  212. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  213. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  214. package/docs/webapi/grabPopupText.mustache +0 -5
  215. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  216. package/docs/webapi/grabSource.mustache +0 -8
  217. package/docs/webapi/grabTextFrom.mustache +0 -10
  218. package/docs/webapi/grabTextFromAll.mustache +0 -9
  219. package/docs/webapi/grabTitle.mustache +0 -8
  220. package/docs/webapi/grabValueFrom.mustache +0 -9
  221. package/docs/webapi/grabValueFromAll.mustache +0 -8
  222. package/docs/webapi/grabWebElement.mustache +0 -9
  223. package/docs/webapi/grabWebElements.mustache +0 -9
  224. package/docs/webapi/moveCursorTo.mustache +0 -12
  225. package/docs/webapi/openNewTab.mustache +0 -7
  226. package/docs/webapi/pressKey.mustache +0 -12
  227. package/docs/webapi/pressKeyDown.mustache +0 -12
  228. package/docs/webapi/pressKeyUp.mustache +0 -12
  229. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  230. package/docs/webapi/refreshPage.mustache +0 -6
  231. package/docs/webapi/resizeWindow.mustache +0 -6
  232. package/docs/webapi/rightClick.mustache +0 -14
  233. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  234. package/docs/webapi/saveScreenshot.mustache +0 -12
  235. package/docs/webapi/say.mustache +0 -10
  236. package/docs/webapi/scrollIntoView.mustache +0 -11
  237. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  238. package/docs/webapi/scrollPageToTop.mustache +0 -6
  239. package/docs/webapi/scrollTo.mustache +0 -12
  240. package/docs/webapi/see.mustache +0 -11
  241. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  242. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  243. package/docs/webapi/seeCookie.mustache +0 -8
  244. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  245. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  246. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  247. package/docs/webapi/seeElement.mustache +0 -8
  248. package/docs/webapi/seeElementInDOM.mustache +0 -8
  249. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  250. package/docs/webapi/seeInField.mustache +0 -12
  251. package/docs/webapi/seeInPopup.mustache +0 -8
  252. package/docs/webapi/seeInSource.mustache +0 -7
  253. package/docs/webapi/seeInTitle.mustache +0 -8
  254. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  255. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  256. package/docs/webapi/seeTextEquals.mustache +0 -9
  257. package/docs/webapi/seeTitleEquals.mustache +0 -8
  258. package/docs/webapi/seeTraffic.mustache +0 -36
  259. package/docs/webapi/selectOption.mustache +0 -21
  260. package/docs/webapi/setCookie.mustache +0 -16
  261. package/docs/webapi/setGeoLocation.mustache +0 -12
  262. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  263. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  264. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  265. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  266. package/docs/webapi/switchTo.mustache +0 -9
  267. package/docs/webapi/switchToNextTab.mustache +0 -10
  268. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  269. package/docs/webapi/type.mustache +0 -21
  270. package/docs/webapi/uncheckOption.mustache +0 -13
  271. package/docs/webapi/wait.mustache +0 -8
  272. package/docs/webapi/waitForClickable.mustache +0 -11
  273. package/docs/webapi/waitForCookie.mustache +0 -9
  274. package/docs/webapi/waitForDetached.mustache +0 -10
  275. package/docs/webapi/waitForDisabled.mustache +0 -6
  276. package/docs/webapi/waitForElement.mustache +0 -11
  277. package/docs/webapi/waitForEnabled.mustache +0 -6
  278. package/docs/webapi/waitForFunction.mustache +0 -17
  279. package/docs/webapi/waitForInvisible.mustache +0 -10
  280. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  281. package/docs/webapi/waitForText.mustache +0 -13
  282. package/docs/webapi/waitForValue.mustache +0 -10
  283. package/docs/webapi/waitForVisible.mustache +0 -10
  284. package/docs/webapi/waitInUrl.mustache +0 -9
  285. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  286. package/docs/webapi/waitToHide.mustache +0 -10
  287. package/docs/webapi/waitUrlEquals.mustache +0 -10
  288. package/lib/helper/AI.js +0 -214
  289. package/lib/listener/enhancedGlobalRetry.js +0 -110
  290. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  291. package/lib/plugin/htmlReporter.js +0 -3648
  292. package/lib/plugin/stepByStepReport.js +0 -427
  293. package/lib/plugin/subtitles.js +0 -89
  294. package/lib/retryCoordinator.js +0 -207
  295. package/typings/promiseBasedTypes.d.ts +0 -9469
  296. package/typings/types.d.ts +0 -11402
@@ -0,0 +1,456 @@
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.name === '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.name === '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
+ for (const pattern of config.ignoreSteps || []) {
441
+ if (step.name.match(pattern)) return true
442
+ }
443
+ return false
444
+ }
445
+
446
+ function generateStepPrefix(step, index) {
447
+ const stepName = step.toString()
448
+ const cleanedName = clearString(stepName)
449
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
450
+ .replace(/_{2,}/g, '_')
451
+ .slice(0, 80)
452
+ .trim()
453
+
454
+ return `${String(index).padStart(4, '0')}_${cleanedName}`
455
+ }
456
+ }
@@ -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
 
@@ -227,14 +228,14 @@ export default function (config = {}) {
227
228
  console.log('Enabled AI analysis')
228
229
  })
229
230
 
230
- event.dispatcher.on(event.all.result, async result => {
231
+ event.dispatcher.on(event.all.result, result => {
231
232
  if (!isMainThread) return // run only on main thread
232
233
  if (!ai.isEnabled) {
233
234
  console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
234
235
  return
235
236
  }
236
237
 
237
- printReport(result)
238
+ recorder.add('analyze:print-ai-report', () => printReport(result), true)
238
239
  })
239
240
 
240
241
  event.dispatcher.on(event.workers.result, async result => {
@@ -248,7 +249,7 @@ export default function (config = {}) {
248
249
  return
249
250
  }
250
251
 
251
- printReport(result)
252
+ await printReport(result)
252
253
  })
253
254
 
254
255
  async function printReport(result) {
@@ -294,7 +295,7 @@ export default function (config = {}) {
294
295
  console.error('Error analyzing failed tests', err)
295
296
  }
296
297
 
297
- if (!Object.keys(container.plugins()).includes('pageInfo')) {
298
+ if (!Object.keys(Container.plugins()).includes('pageInfo')) {
298
299
  console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
299
300
  }
300
301
  }
@@ -353,7 +354,7 @@ function serializeError(error) {
353
354
  errorMessage +=
354
355
  '\n' +
355
356
  error.stack
356
- .replace(global.codecept_dir || '', '.')
357
+ .replace(store.codeceptDir || '', '.')
357
358
  .split('\n')
358
359
  .map(line => line.replace(ansiRegExp(), ''))
359
360
  .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
  }
@@ -377,7 +377,7 @@ export default function (config) {
377
377
  }
378
378
 
379
379
  if (!config.saveToFile) return
380
- const cookieFile = path.join(global.output_dir, `${name}_session.json`)
380
+ const cookieFile = path.join(store.outputDir, `${name}_session.json`)
381
381
 
382
382
  if (!fileExists(cookieFile)) {
383
383
  return
@@ -412,7 +412,7 @@ export default function (config) {
412
412
 
413
413
  function loadCookiesFromFile(config) {
414
414
  for (const name in config.users) {
415
- const fileName = path.join(global.output_dir, `${name}_session.json`)
415
+ const fileName = path.join(store.outputDir, `${name}_session.json`)
416
416
  if (!fileExists(fileName)) continue
417
417
  const data = fs.readFileSync(fileName).toString()
418
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
+ }