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,453 @@
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 firstFailedStepSaved = false
96
+
97
+ const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
98
+
99
+ if (config.captureDebugOutput) {
100
+ const originalDebug = output.debug
101
+ output.debug = function (...args) {
102
+ debugOutput.push(args.join(' '))
103
+ originalDebug.apply(output, args)
104
+ }
105
+ }
106
+
107
+ event.dispatcher.on(event.suite.before, suite => {
108
+ stepNum = -1
109
+ })
110
+
111
+ event.dispatcher.on(event.test.before, test => {
112
+ let title
113
+ try {
114
+ title = test.fullTitle ? test.fullTitle() : test.title
115
+ } catch (err) {
116
+ title = test.title
117
+ }
118
+ dir = traceDirFor(test.file, title, reportDir)
119
+ mkdirp.sync(dir)
120
+ deleteDir(dir)
121
+ mkdirp.sync(dir)
122
+ stepNum = 0
123
+ error = null
124
+ steps = []
125
+ debugOutput = []
126
+ savedSteps.clear()
127
+ currentTest = test
128
+ testStartTime = Date.now()
129
+ currentUrl = null
130
+ testFailed = false
131
+ firstFailedStepSaved = false
132
+ })
133
+
134
+ event.dispatcher.on(event.step.after, step => {
135
+ if (!currentTest) return
136
+ if (step.status === 'failed') {
137
+ testFailed = true
138
+ }
139
+ if (step.status === 'queued' && testFailed) {
140
+ output.debug(`aiTrace: Skipping queued step "${step.toString()}" - testFailed: ${testFailed}`)
141
+ return
142
+ }
143
+ if (step.status === 'failed' && firstFailedStepSaved) {
144
+ output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
145
+ return
146
+ }
147
+
148
+ // on= filtering
149
+ if (trigger.on === 'fail') return // failed steps handled by step.failed
150
+ if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
151
+ if (trigger.on === 'url') {
152
+ recorder.add('aiTrace:url check', async () => {
153
+ try {
154
+ if (!helper.grabCurrentUrl) return
155
+ const url = await helper.grabCurrentUrl()
156
+ if (!matchUrl(url, trigger.pattern)) return
157
+ await persistStep(step)
158
+ } catch (err) {
159
+ output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
160
+ }
161
+ }, true)
162
+ return
163
+ }
164
+
165
+ const stepPersistPromise = persistStep(step).catch(err => {
166
+ output.debug(`aiTrace: Error saving step: ${err.message}`)
167
+ })
168
+ recorder.add(`wait aiTrace step persistence: ${step.toString()}`, () => stepPersistPromise, true)
169
+ })
170
+
171
+ event.dispatcher.on(event.step.failed, async step => {
172
+ if (!currentTest) return
173
+ if (step.status === 'queued' && testFailed) {
174
+ output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
175
+ return
176
+ }
177
+ if (firstFailedStepSaved) {
178
+ output.debug(`aiTrace: Skipping subsequent failed step "${step.toString()}" - already saved first failed step`)
179
+ return
180
+ }
181
+
182
+ const stepKey = step.toString()
183
+ if (savedSteps.has(stepKey)) {
184
+ const existingStep = steps.find(s => s.step === stepKey)
185
+ if (!existingStep) {
186
+ output.debug(`aiTrace: Step "${stepKey}" marked as saved but not found in steps array`)
187
+ return
188
+ }
189
+ existingStep.status = 'failed'
190
+
191
+ try {
192
+ await captureArtifactsForStep(step, existingStep, existingStep.prefix)
193
+ } catch (err) {
194
+ output.debug(`aiTrace: Error updating failed step: ${err.message}`)
195
+ }
196
+ } else {
197
+ if (stepNum === -1) return
198
+ if (isStepIgnored(step)) return
199
+ if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
200
+
201
+ const stepPrefix = generateStepPrefix(step, stepNum)
202
+ stepNum++
203
+
204
+ const stepData = {
205
+ step: stepKey,
206
+ status: 'failed',
207
+ prefix: stepPrefix,
208
+ artifacts: {},
209
+ meta: {},
210
+ debugOutput: [],
211
+ }
212
+
213
+ if (step.startTime && step.endTime) {
214
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
215
+ }
216
+
217
+ savedSteps.add(stepKey)
218
+ steps.push(stepData)
219
+ firstFailedStepSaved = true
220
+
221
+ try {
222
+ await captureArtifactsForStep(step, stepData, stepPrefix)
223
+ } catch (err) {
224
+ output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
225
+ }
226
+ }
227
+ })
228
+
229
+ event.dispatcher.on(event.test.passed, test => {
230
+ if (config.deleteSuccessful) {
231
+ deleteDir(dir)
232
+ return
233
+ }
234
+ persist(test, 'passed')
235
+ })
236
+
237
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
238
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
239
+ return
240
+ }
241
+ persist(test, 'failed')
242
+ })
243
+
244
+ async function persistStep(step) {
245
+ if (stepNum === -1) return
246
+ if (isStepIgnored(step)) return
247
+ if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
248
+
249
+ const stepKey = step.toString()
250
+
251
+ if (savedSteps.has(stepKey)) {
252
+ const existingStep = steps.find(s => s.step === stepKey)
253
+ if (existingStep && step.status === 'failed') {
254
+ existingStep.status = 'failed'
255
+ step.artifacts = {}
256
+ await captureArtifactsForStep(step, existingStep, existingStep.prefix)
257
+ }
258
+ return
259
+ }
260
+ savedSteps.add(stepKey)
261
+
262
+ const stepPrefix = generateStepPrefix(step, stepNum)
263
+ stepNum++
264
+
265
+ const stepData = {
266
+ step: step.toString(),
267
+ status: step.status,
268
+ prefix: stepPrefix,
269
+ artifacts: {},
270
+ meta: {},
271
+ debugOutput: [],
272
+ }
273
+
274
+ if (step.startTime && step.endTime) {
275
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
276
+ }
277
+
278
+ if (config.captureDebugOutput && debugOutput.length > 0) {
279
+ stepData.debugOutput = [...debugOutput]
280
+ debugOutput = []
281
+ }
282
+
283
+ await captureArtifactsForStep(step, stepData, stepPrefix)
284
+ steps.push(stepData)
285
+ }
286
+
287
+ async function captureArtifactsForStep(step, stepData, stepPrefix) {
288
+ if (!step.artifacts) {
289
+ step.artifacts = {}
290
+ }
291
+
292
+ let browserAvailable = true
293
+
294
+ try {
295
+ try {
296
+ if (helper.grabCurrentUrl) {
297
+ const url = await helper.grabCurrentUrl()
298
+ stepData.meta.url = url
299
+ currentUrl = url
300
+ }
301
+ } catch (err) {
302
+ browserAvailable = false
303
+ output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
304
+ }
305
+
306
+ let preExistingScreenshot = false
307
+ if (step.artifacts?.screenshot) {
308
+ const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
309
+ ? step.artifacts.screenshot
310
+ : path.resolve(dir, step.artifacts.screenshot)
311
+ const screenshotFile = path.basename(screenshotPath)
312
+ stepData.artifacts.screenshot = screenshotFile
313
+ step.artifacts.screenshot = screenshotPath
314
+ preExistingScreenshot = true
315
+
316
+ if (!fs.existsSync(screenshotPath)) {
317
+ try {
318
+ await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
319
+ } catch (err) {
320
+ output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
321
+ }
322
+ }
323
+ }
324
+
325
+ const captured = await captureSnapshot(helper, {
326
+ dir,
327
+ prefix: stepPrefix,
328
+ fullPage: config.fullPageScreenshots,
329
+ captureHTML: config.captureHTML && browserAvailable,
330
+ captureARIA: config.captureARIA && browserAvailable,
331
+ captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
332
+ captureStorage: false,
333
+ })
334
+
335
+ if (!preExistingScreenshot && captured.screenshot) {
336
+ stepData.artifacts.screenshot = captured.screenshot
337
+ step.artifacts.screenshot = path.join(dir, captured.screenshot)
338
+ }
339
+ if (step.artifacts?.html) {
340
+ stepData.artifacts.html = step.artifacts.html
341
+ } else if (captured.html) {
342
+ stepData.artifacts.html = captured.html
343
+ }
344
+ if (captured.aria) stepData.artifacts.aria = captured.aria
345
+ if (captured.console) {
346
+ stepData.artifacts.console = captured.console
347
+ stepData.meta.consoleCount = captured.consoleCount
348
+ }
349
+ } catch (err) {
350
+ output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
351
+ }
352
+ }
353
+
354
+ function persist(test, status) {
355
+ if (!steps.length) {
356
+ output.debug('aiTrace: No steps to save in trace')
357
+ return
358
+ }
359
+
360
+ // on=test: only render the last step in markdown; artifacts of earlier steps
361
+ // remain on disk unreferenced.
362
+ if (trigger.on === 'test') {
363
+ steps = steps.slice(-1)
364
+ }
365
+
366
+ const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
367
+
368
+ let markdown = `file: ${test.file || 'unknown'}\n`
369
+ markdown += `name: ${test.title}\n`
370
+ markdown += `time: ${testDuration}s\n`
371
+ markdown += `---\n\n`
372
+
373
+ if (status === 'failed') {
374
+ if (test.art && test.art.message) {
375
+ markdown += `Error: ${test.art.message}\n\n`
376
+ }
377
+ if (test.art && test.art.stack) {
378
+ markdown += `${test.art.stack}\n\n`
379
+ }
380
+ markdown += `---\n\n`
381
+ }
382
+
383
+ if (config.captureDebugOutput && debugOutput.length > 0) {
384
+ markdown += `CodeceptJS Debug Output:\n\n`
385
+ debugOutput.forEach(line => {
386
+ markdown += `> ${line}\n`
387
+ })
388
+ markdown += `\n---\n\n`
389
+ }
390
+
391
+ steps.forEach((stepData, index) => {
392
+ const stepAnchor = clearString(stepData.step).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)
393
+ markdown += `### Step ${index + 1}: ${stepData.step}\n`
394
+ markdown += `<a id="${stepAnchor}"></a>\n`
395
+
396
+ if (stepData.meta.duration) {
397
+ markdown += ` > duration: ${stepData.meta.duration}\n`
398
+ }
399
+
400
+ if (stepData.meta.url) {
401
+ markdown += ` > navigated to ${stepData.meta.url}\n`
402
+ }
403
+
404
+ if (config.captureDebugOutput && stepData.debugOutput && stepData.debugOutput.length > 0) {
405
+ stepData.debugOutput.forEach(line => {
406
+ markdown += ` > ${line}\n`
407
+ })
408
+ }
409
+
410
+ const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
411
+ if (links) markdown += links + '\n'
412
+
413
+ if (config.captureHTTP) {
414
+ if (test.artifacts && test.artifacts.har) {
415
+ const harPath = path.relative(reportDir, test.artifacts.har)
416
+ markdown += ` > HTTP: see [HAR file](../${harPath}) for network requests\n`
417
+ } else if (test.artifacts && test.artifacts.trace) {
418
+ const tracePath = path.relative(reportDir, test.artifacts.trace)
419
+ markdown += ` > HTTP: see [Playwright trace](../${tracePath}) for network requests\n`
420
+ }
421
+ }
422
+
423
+ markdown += `\n`
424
+ })
425
+
426
+ const traceFile = path.join(dir, 'trace.md')
427
+ fs.writeFileSync(traceFile, markdown)
428
+
429
+ output.print(`Trace Saved: file://${traceFile}`)
430
+
431
+ if (!test.artifacts) test.artifacts = {}
432
+ test.artifacts.aiTrace = traceFile
433
+ }
434
+
435
+ function isStepIgnored(step) {
436
+ if (!config.ignoreSteps) return false
437
+ for (const pattern of config.ignoreSteps || []) {
438
+ if (step.name.match(pattern)) return true
439
+ }
440
+ return false
441
+ }
442
+
443
+ function generateStepPrefix(step, index) {
444
+ const stepName = step.toString()
445
+ const cleanedName = clearString(stepName)
446
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
447
+ .replace(/_{2,}/g, '_')
448
+ .slice(0, 80)
449
+ .trim()
450
+
451
+ return `${String(index).padStart(4, '0')}_${cleanedName}`
452
+ }
453
+ }
@@ -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
  }
@@ -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
+ }