codeceptjs 4.0.0-rc.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +743 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +198 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +194 -2
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +63 -70
  126. package/lib/helper/Puppeteer.js +20 -109
  127. package/lib/helper/WebDriver.js +13 -30
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +10 -3
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +7 -0
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +0 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -1,22 +1,26 @@
1
- import crypto from 'crypto'
2
1
  import fs from 'fs'
3
2
  import { mkdirp } from 'mkdirp'
4
3
  import path from 'path'
5
- import { fileURLToPath } from 'url'
6
4
 
5
+ import store from '../store.js'
7
6
  import Container from '../container.js'
8
7
  import recorder from '../recorder.js'
9
8
  import event from '../event.js'
10
9
  import output from '../output.js'
11
10
  import { deleteDir, clearString } from '../utils.js'
12
- import colors from 'chalk'
13
-
14
- const supportedHelpers = Container.STANDARD_ACTING_HELPERS
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'
15
18
 
16
19
  const defaultConfig = {
20
+ on: 'step',
17
21
  deleteSuccessful: false,
18
22
  fullPageScreenshots: false,
19
- output: global.output_dir,
23
+ output: store.outputDir,
20
24
  captureHTML: true,
21
25
  captureARIA: true,
22
26
  captureBrowserLogs: true,
@@ -52,20 +56,26 @@ const defaultConfig = {
52
56
  * * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
53
57
  * * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
54
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=...`
55
68
  *
56
69
  * @param {*} config
57
70
  */
58
- export default function (config) {
59
- const helpers = Container.helpers()
60
- let helper
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
61
75
 
62
76
  config = Object.assign(defaultConfig, config)
63
77
 
64
- for (const helperName of supportedHelpers) {
65
- if (Object.keys(helpers).indexOf(helperName) > -1) {
66
- helper = helpers[helperName]
67
- }
68
- }
78
+ const helper = pickActingHelper(Container.helpers())
69
79
 
70
80
  if (!helper) {
71
81
  output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
@@ -82,9 +92,10 @@ export default function (config) {
82
92
  let testStartTime
83
93
  let currentUrl = null
84
94
  let testFailed = false
95
+ let pendingArtifactCapture = null
85
96
  let firstFailedStepSaved = false
86
97
 
87
- const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output
98
+ const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
88
99
 
89
100
  if (config.captureDebugOutput) {
90
101
  const originalDebug = output.debug
@@ -105,13 +116,7 @@ export default function (config) {
105
116
  } catch (err) {
106
117
  title = test.title
107
118
  }
108
- const testTitle = clearString(title).slice(0, 200)
109
- const uniqueHash = crypto
110
- .createHash('sha256')
111
- .update(test.file + test.title)
112
- .digest('hex')
113
- .slice(0, 8)
114
- dir = path.join(reportDir, `trace_${testTitle}_${uniqueHash}`)
119
+ dir = traceDirFor(test.file, title, reportDir)
115
120
  mkdirp.sync(dir)
116
121
  deleteDir(dir)
117
122
  mkdirp.sync(dir)
@@ -125,6 +130,7 @@ export default function (config) {
125
130
  currentUrl = null
126
131
  testFailed = false
127
132
  firstFailedStepSaved = false
133
+ pendingArtifactCapture = null
128
134
  })
129
135
 
130
136
  event.dispatcher.on(event.step.after, step => {
@@ -140,13 +146,30 @@ export default function (config) {
140
146
  output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
141
147
  return
142
148
  }
143
- const stepPersistPromise = persistStep(step).catch(err => {
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 => {
144
168
  output.debug(`aiTrace: Error saving step: ${err.message}`)
145
- })
146
- recorder.add(`wait aiTrace step persistence: ${step.toString()}`, () => stepPersistPromise, true)
169
+ }), true)
147
170
  })
148
171
 
149
- event.dispatcher.on(event.step.failed, async step => {
172
+ event.dispatcher.on(event.step.failed, step => {
150
173
  if (!currentTest) return
151
174
  if (step.status === 'queued' && testFailed) {
152
175
  output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
@@ -166,15 +189,13 @@ export default function (config) {
166
189
  }
167
190
  existingStep.status = 'failed'
168
191
 
169
- try {
170
- await captureArtifactsForStep(step, existingStep, existingStep.prefix)
171
- } catch (err) {
192
+ pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
172
193
  output.debug(`aiTrace: Error updating failed step: ${err.message}`)
173
- }
194
+ })
174
195
  } else {
175
196
  if (stepNum === -1) return
176
197
  if (isStepIgnored(step)) return
177
- if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
198
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
178
199
 
179
200
  const stepPrefix = generateStepPrefix(step, stepNum)
180
201
  stepNum++
@@ -196,11 +217,9 @@ export default function (config) {
196
217
  steps.push(stepData)
197
218
  firstFailedStepSaved = true
198
219
 
199
- try {
200
- await captureArtifactsForStep(step, stepData, stepPrefix)
201
- } catch (err) {
220
+ pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
202
221
  output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
203
- }
222
+ })
204
223
  }
205
224
  })
206
225
 
@@ -216,13 +235,19 @@ export default function (config) {
216
235
  if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
217
236
  return
218
237
  }
219
- persist(test, 'failed')
238
+ recorder.add('aiTrace:persist failed', async () => {
239
+ if (pendingArtifactCapture) {
240
+ await pendingArtifactCapture
241
+ pendingArtifactCapture = null
242
+ }
243
+ persist(test, 'failed')
244
+ }, true)
220
245
  })
221
246
 
222
247
  async function persistStep(step) {
223
248
  if (stepNum === -1) return
224
249
  if (isStepIgnored(step)) return
225
- if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
250
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
226
251
 
227
252
  const stepKey = step.toString()
228
253
 
@@ -281,6 +306,7 @@ export default function (config) {
281
306
  output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
282
307
  }
283
308
 
309
+ let preExistingScreenshot = false
284
310
  if (step.artifacts?.screenshot) {
285
311
  const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
286
312
  ? step.artifacts.screenshot
@@ -288,6 +314,7 @@ export default function (config) {
288
314
  const screenshotFile = path.basename(screenshotPath)
289
315
  stepData.artifacts.screenshot = screenshotFile
290
316
  step.artifacts.screenshot = screenshotPath
317
+ preExistingScreenshot = true
291
318
 
292
319
  if (!fs.existsSync(screenshotPath)) {
293
320
  try {
@@ -296,58 +323,31 @@ export default function (config) {
296
323
  output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
297
324
  }
298
325
  }
299
- } else {
300
- try {
301
- const screenshotFile = `${stepPrefix}_screenshot.png`
302
- const screenshotPath = path.join(dir, screenshotFile)
303
- await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
304
-
305
- stepData.artifacts.screenshot = screenshotFile
306
- step.artifacts.screenshot = screenshotPath
307
- } catch (err) {
308
- output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
309
- }
310
326
  }
311
327
 
312
- // Save HTML
313
- if (config.captureHTML && helper.grabSource && browserAvailable) {
314
- if (!step.artifacts?.html) {
315
- try {
316
- const html = await helper.grabSource()
317
- const htmlFile = `${stepPrefix}_page.html`
318
- fs.writeFileSync(path.join(dir, htmlFile), html)
319
- stepData.artifacts.html = htmlFile
320
- } catch (err) {
321
- output.debug(`aiTrace: Could not capture HTML: ${err.message}`)
322
- }
323
- } else {
324
- stepData.artifacts.html = step.artifacts.html
325
- }
326
- }
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
+ })
327
337
 
328
- // Save ARIA snapshot
329
- if (config.captureARIA && helper.grabAriaSnapshot && browserAvailable) {
330
- try {
331
- const aria = await helper.grabAriaSnapshot()
332
- const ariaFile = `${stepPrefix}_aria.txt`
333
- fs.writeFileSync(path.join(dir, ariaFile), aria)
334
- stepData.artifacts.aria = ariaFile
335
- } catch (err) {
336
- output.debug(`aiTrace: Could not capture ARIA snapshot: ${err.message}`)
337
- }
338
+ if (!preExistingScreenshot && captured.screenshot) {
339
+ stepData.artifacts.screenshot = captured.screenshot
340
+ step.artifacts.screenshot = path.join(dir, captured.screenshot)
338
341
  }
339
-
340
- // Save browser logs
341
- if (config.captureBrowserLogs && helper.grabBrowserLogs && browserAvailable) {
342
- try {
343
- const logs = await helper.grabBrowserLogs()
344
- const logsFile = `${stepPrefix}_console.json`
345
- fs.writeFileSync(path.join(dir, logsFile), JSON.stringify(logs || [], null, 2))
346
- stepData.artifacts.console = logsFile
347
- stepData.meta.consoleCount = logs ? logs.length : 0
348
- } catch (err) {
349
- output.debug(`aiTrace: Could not capture browser logs: ${err.message}`)
350
- }
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
351
  }
352
352
  } catch (err) {
353
353
  output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
@@ -360,6 +360,12 @@ export default function (config) {
360
360
  return
361
361
  }
362
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
+
363
369
  const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
364
370
 
365
371
  let markdown = `file: ${test.file || 'unknown'}\n`
@@ -404,22 +410,8 @@ export default function (config) {
404
410
  })
405
411
  }
406
412
 
407
- if (stepData.artifacts.html) {
408
- markdown += ` > [HTML](./${stepData.artifacts.html})\n`
409
- }
410
-
411
- if (stepData.artifacts.aria) {
412
- markdown += ` > [ARIA Snapshot](./${stepData.artifacts.aria})\n`
413
- }
414
-
415
- if (stepData.artifacts.screenshot) {
416
- markdown += ` > [Screenshot](./${stepData.artifacts.screenshot})\n`
417
- }
418
-
419
- if (stepData.artifacts.console) {
420
- const count = stepData.meta.consoleCount || 0
421
- markdown += ` > [Browser Logs](./${stepData.artifacts.console}) (${count} entries)\n`
422
- }
413
+ const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
414
+ if (links) markdown += links + '\n'
423
415
 
424
416
  if (config.captureHTTP) {
425
417
  if (test.artifacts && test.artifacts.har) {
@@ -437,7 +429,7 @@ export default function (config) {
437
429
  const traceFile = path.join(dir, 'trace.md')
438
430
  fs.writeFileSync(traceFile, markdown)
439
431
 
440
- output.print(`🤖 AI Trace: ${colors.white.bold(`file://${traceFile}`)}`)
432
+ output.print(`Trace Saved: file://${traceFile}`)
441
433
 
442
434
  if (!test.artifacts) test.artifacts = {}
443
435
  test.artifacts.aiTrace = traceFile
@@ -445,8 +437,9 @@ export default function (config) {
445
437
 
446
438
  function isStepIgnored(step) {
447
439
  if (!config.ignoreSteps) return false
440
+ if (!step.title) return false
448
441
  for (const pattern of config.ignoreSteps || []) {
449
- if (step.name.match(pattern)) return true
442
+ if (step.title.match(pattern)) return true
450
443
  }
451
444
  return false
452
445
  }
@@ -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
  }
@@ -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
+ }
@@ -0,0 +1,159 @@
1
+ import Container from '../container.js'
2
+
3
+ const RESERVED_NAMES = new Set(['I', 'test', 'suite'])
4
+ const SHORTHAND_PROPERTIES = new Set(['page', 'browser', 'browserContext', 'context'])
5
+
6
+ const defaultConfig = {
7
+ inject: {},
8
+ }
9
+
10
+ /**
11
+ * Exposes properties from helper instances as injectable test arguments.
12
+ * Use it to access the underlying Playwright/Puppeteer `page`, the wdio `browser` client,
13
+ * or any other helper internal directly from a Scenario:
14
+ *
15
+ * ```js
16
+ * Scenario('listen for requests', async ({ I, page, browser }) => {
17
+ * page.on('request', r => console.log(r.url()))
18
+ * await page.evaluate(() => 1 + 1)
19
+ * I.amOnPage('/')
20
+ * })
21
+ * ```
22
+ *
23
+ * The injected value is a live proxy: every property access reads the *current*
24
+ * helper property, so mid-test reassignments (popups, `switchToNextTab`,
25
+ * `openNewTab`) are reflected automatically. Calls are not wrapped as
26
+ * CodeceptJS steps — `await page.evaluate(...)` runs as native Playwright.
27
+ *
28
+ * #### Configuration
29
+ *
30
+ * `inject` maps an injection name to a `HelperName.propertyName` string. A
31
+ * value with no dot is shorthand for "first configured browser helper that
32
+ * exposes this property" (allowed properties: `page`, `browser`,
33
+ * `browserContext`, `context`).
34
+ *
35
+ * ```js
36
+ * plugins: {
37
+ * expose: {
38
+ * enabled: true,
39
+ * inject: {
40
+ * page: 'Playwright.page',
41
+ * browser: 'Playwright.browser',
42
+ * browserContext: 'Playwright.browserContext',
43
+ * frame: 'Playwright.context', // current frame set by switchTo
44
+ * wdio: 'WebDriver.browser',
45
+ * }
46
+ * }
47
+ * }
48
+ * ```
49
+ *
50
+ * Shorthand:
51
+ *
52
+ * ```js
53
+ * plugins: {
54
+ * expose: {
55
+ * enabled: true,
56
+ * inject: {
57
+ * page: 'page', // resolves to Playwright.page or Puppeteer.page
58
+ * }
59
+ * }
60
+ * }
61
+ * ```
62
+ *
63
+ * #### Caveats
64
+ *
65
+ * - The injected value is a `Proxy`, not the actual `Page`/`Browser` instance,
66
+ * so `page instanceof Page` is `false`. Use duck typing instead.
67
+ * - Cached method references lose the live binding. Call `page.click(...)`,
68
+ * not `const click = page.click; click(...)`.
69
+ * - In dry-run mode the underlying helper property is `undefined`; accessing
70
+ * any property on the proxy returns `undefined` rather than throwing.
71
+ */
72
+ export default function (config = {}) {
73
+ config = { ...defaultConfig, ...config }
74
+
75
+ const mappings = parseMappings(config.inject)
76
+
77
+ const support = {}
78
+ for (const [name, { helperName, property }] of Object.entries(mappings)) {
79
+ support[name] = makeLiveProxy(helperName, property)
80
+ }
81
+ Container.append({ support })
82
+ }
83
+
84
+ function parseMappings(inject) {
85
+ const out = {}
86
+ for (const [name, value] of Object.entries(inject || {})) {
87
+ if (RESERVED_NAMES.has(name)) {
88
+ throw new Error(`expose plugin: inject name '${name}' is reserved`)
89
+ }
90
+ if (typeof value !== 'string' || !value) {
91
+ throw new Error(`expose plugin: inject value for '${name}' must be a non-empty string`)
92
+ }
93
+
94
+ let helperName
95
+ let property
96
+
97
+ if (value.includes('.')) {
98
+ const dot = value.indexOf('.')
99
+ helperName = value.slice(0, dot)
100
+ property = value.slice(dot + 1)
101
+ if (!helperName || !property) {
102
+ throw new Error(`expose plugin: invalid inject value '${value}' for '${name}' (expected 'HelperName.propertyName')`)
103
+ }
104
+ if (!Container.helpers(helperName)) {
105
+ throw new Error(`expose plugin: helper '${helperName}' is not configured (needed for inject '${name}')`)
106
+ }
107
+ } else {
108
+ property = value
109
+ if (!SHORTHAND_PROPERTIES.has(property)) {
110
+ throw new Error(`expose plugin: shorthand '${property}' is not a known helper property for '${name}' (use 'HelperName.${property}' instead)`)
111
+ }
112
+ helperName = Container.STANDARD_ACTING_HELPERS.find(h => Container.helpers(h))
113
+ if (!helperName) {
114
+ throw new Error(`expose plugin: no standard browser helper configured (needed for inject '${name}')`)
115
+ }
116
+ }
117
+
118
+ out[name] = { helperName, property }
119
+ }
120
+ return out
121
+ }
122
+
123
+ function makeLiveProxy(helperName, property) {
124
+ const resolve = () => Container.helpers(helperName)?.[property]
125
+ return new Proxy(function () {}, {
126
+ get(_, prop) {
127
+ const target = resolve()
128
+ if (target == null) return undefined
129
+ const value = target[prop]
130
+ if (typeof value === 'function') return value.bind(target)
131
+ return value
132
+ },
133
+ has(_, prop) {
134
+ const target = resolve()
135
+ return target != null && prop in target
136
+ },
137
+ apply(_, thisArg, args) {
138
+ const target = resolve()
139
+ return target?.apply(thisArg, args)
140
+ },
141
+ set(_, prop, value) {
142
+ const target = resolve()
143
+ if (target != null) target[prop] = value
144
+ return true
145
+ },
146
+ getPrototypeOf() {
147
+ const target = resolve()
148
+ return target != null ? Object.getPrototypeOf(target) : null
149
+ },
150
+ ownKeys() {
151
+ const target = resolve()
152
+ return target != null ? Reflect.ownKeys(target) : []
153
+ },
154
+ getOwnPropertyDescriptor(_, prop) {
155
+ const target = resolve()
156
+ return target != null ? Object.getOwnPropertyDescriptor(target, prop) : undefined
157
+ },
158
+ })
159
+ }