codeceptjs 4.0.0-rc.8 → 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 +195 -3
  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 +96 -115
  126. package/lib/helper/Puppeteer.js +43 -131
  127. package/lib/helper/WebDriver.js +42 -52
  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 +58 -0
  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 +15 -2
  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 +19 -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
package/lib/config.js CHANGED
@@ -15,8 +15,9 @@ const defaultConfig = {
15
15
  hooks: [],
16
16
  gherkin: {},
17
17
  plugins: {
18
- screenshotOnFail: {
19
- enabled: true, // will be disabled by default in 2.0
18
+ screenshot: {
19
+ enabled: true,
20
+ on: 'fail',
20
21
  },
21
22
  },
22
23
  stepTimeout: 0,
@@ -32,9 +33,27 @@ const defaultConfig = {
32
33
  ],
33
34
  }
34
35
 
36
+ // Array<{ fn: (cfg) => void, ran: boolean, error?: Error }>
35
37
  let hooks = []
36
38
  let config = {}
37
39
 
40
+ // Apply a single hook against `cfg`, swallowing errors so one broken hook
41
+ // can't take down the whole run. The failure is logged through the
42
+ // framework's own output module (when available) so it shows up in test
43
+ // reports; the hook is still marked ran so it doesn't get retried.
44
+ function applyHook(hook, cfg) {
45
+ try {
46
+ hook.fn(cfg)
47
+ } catch (err) {
48
+ hook.error = err
49
+ const out = globalThis.codeceptjs?.output
50
+ if (out && typeof out.error === 'function') out.error(`config hook failed: ${err.message}`)
51
+ else console.error('config hook failed:', err)
52
+ } finally {
53
+ hook.ran = true
54
+ }
55
+ }
56
+
38
57
  const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
39
58
 
40
59
  /**
@@ -49,7 +68,11 @@ class Config {
49
68
  */
50
69
  static create(newConfig) {
51
70
  config = deepMerge(deepClone(defaultConfig), newConfig)
52
- hooks.forEach(f => f(config))
71
+ // Re-apply every hook against the freshly built config; hooks added later
72
+ // (e.g. from plugin boot) stay pending until runPendingHooks. Array
73
+ // iterators re-check length on each step, so hooks pushed during a hook
74
+ // execution are visited in this same pass.
75
+ for (const hook of hooks) applyHook(hook, config)
53
76
  return config
54
77
  }
55
78
 
@@ -121,7 +144,48 @@ class Config {
121
144
  }
122
145
 
123
146
  static addHook(fn) {
124
- hooks.push(fn)
147
+ hooks.push({ fn, ran: false })
148
+ }
149
+
150
+ /**
151
+ * Run every hook that hasn't been applied to the current config yet.
152
+ * Hooks added after `Config.create()` (e.g. from plugin boot code) stay
153
+ * pending until this is called; once it runs, they're marked applied so
154
+ * subsequent calls are no-ops. Hooks added while pending hooks are running
155
+ * are picked up in the same pass (the array iterator re-checks length).
156
+ *
157
+ * Failures are logged through `output.error` and don't abort the loop —
158
+ * a broken hook can't poison the run, but its error is visible.
159
+ *
160
+ * @param {Object<string, *>} [cfg] target config (defaults to the live singleton)
161
+ * @return {boolean} true if any hook ran
162
+ */
163
+ static runPendingHooks(cfg = config) {
164
+ let ran = false
165
+ for (const hook of hooks) {
166
+ if (hook.ran) continue
167
+ applyHook(hook, cfg)
168
+ ran = true
169
+ }
170
+ return ran
171
+ }
172
+
173
+ /**
174
+ * Number of registered config hooks. Useful for snapshotting before a phase
175
+ * (e.g. plugin loading) and re-running only the hooks added during it.
176
+ * @return {number}
177
+ */
178
+ static hooksCount() {
179
+ return hooks.length
180
+ }
181
+
182
+ /**
183
+ * Run hooks in `[fromIndex, end)` against the given config object, mutating it.
184
+ * @param {number} fromIndex
185
+ * @param {Object<string, *>} cfg
186
+ */
187
+ static runHooksFrom(fromIndex, cfg) {
188
+ for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
125
189
  }
126
190
 
127
191
  /**
@@ -150,6 +214,15 @@ async function loadConfigFile(configFile) {
150
214
  const require = createRequire(import.meta.url)
151
215
  const extensionName = path.extname(configFile)
152
216
 
217
+ // Populate the in-process registry that packages like @codeceptjs/configure
218
+ // look up at config-import time (their proxies throw if `globalThis.codeceptjs`
219
+ // is missing). initCodeceptGlobals sets this too, but only later during
220
+ // bootstrap — config files are imported here first.
221
+ if (!globalThis.codeceptjs) {
222
+ const indexModule = await import('./index.js')
223
+ globalThis.codeceptjs = indexModule.default || indexModule
224
+ }
225
+
153
226
  // .conf.js config file
154
227
  if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
155
228
  let configModule
package/lib/container.js CHANGED
@@ -15,9 +15,15 @@ import store from './store.js'
15
15
  import Result from './result.js'
16
16
  import ai from './ai.js'
17
17
  import actorFactory from './actor.js'
18
+ import Config from './config.js'
18
19
 
19
20
  let asyncHelperPromise
20
21
 
22
+ let beforeCalledSet = new Set()
23
+
24
+ export function getBeforeCalledSet() { return beforeCalledSet }
25
+ export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
26
+
21
27
  let container = {
22
28
  helpers: {},
23
29
  support: {},
@@ -116,6 +122,18 @@ class Container {
116
122
  // Wait for all async helpers to finish loading and populate the actor
117
123
  await asyncHelperPromise
118
124
 
125
+ // Plugins may have registered Config hooks during their boot. Run anything
126
+ // that hasn't been applied yet and re-feed the mutated helper config to the
127
+ // already-instantiated helpers.
128
+ if (Config.runPendingHooks(config)) {
129
+ for (const name of Object.keys(container.helpers)) {
130
+ const helper = container.helpers[name]
131
+ if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
132
+ helper._setConfig(config.helpers[name])
133
+ }
134
+ }
135
+ }
136
+
119
137
  if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
120
138
  if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
121
139
  if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
@@ -150,10 +168,23 @@ class Container {
150
168
  if (!name) {
151
169
  return container.proxySupport
152
170
  }
153
- // Always return the proxy to ensure MetaStep creation works
171
+ if (typeof container.support[name] === 'function') {
172
+ return container.support[name]
173
+ }
154
174
  return container.proxySupport[name]
155
175
  }
156
176
 
177
+ /**
178
+ * Get raw (non-proxied) support objects for direct access.
179
+ * Used by listeners to call lifecycle hooks without MetaStep wrapping.
180
+ *
181
+ * @api
182
+ * @returns {object}
183
+ */
184
+ static supportObjects() {
185
+ return container.support
186
+ }
187
+
157
188
  /**
158
189
  * Get all helpers or get a helper by name
159
190
  *
@@ -183,7 +214,7 @@ class Container {
183
214
  * @api
184
215
  */
185
216
  static tsFileMapping() {
186
- return container.tsFileMapping
217
+ return store.tsFileMapping
187
218
  }
188
219
 
189
220
  /**
@@ -426,11 +457,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
426
457
  tempJsFile = allTempFiles
427
458
  fileMapping = mapping
428
459
  // Store file mapping in container for runtime error fixing (merge with existing)
429
- if (!container.tsFileMapping) {
430
- container.tsFileMapping = new Map()
460
+ if (!store.tsFileMapping) {
461
+ store.tsFileMapping = new Map()
431
462
  }
432
463
  for (const [key, value] of mapping.entries()) {
433
- container.tsFileMapping.set(key, value)
464
+ store.tsFileMapping.set(key, value)
434
465
  }
435
466
  } catch (tsError) {
436
467
  throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
@@ -542,6 +573,19 @@ function createSupportObjects(config) {
542
573
  let currentValue = currentObject[prop]
543
574
 
544
575
  if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
576
+ if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
577
+ beforeCalledSet.add(name)
578
+ const originalValue = currentValue
579
+ const wrappedValue = async function (...args) {
580
+ await currentObject._before()
581
+ return originalValue.apply(currentObject, args)
582
+ }
583
+ const ms = new MetaStep(name, prop)
584
+ ms.setContext(currentObject)
585
+ debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
586
+ return ms.run.bind(ms, asyncWrapper(wrappedValue))
587
+ }
588
+
545
589
  const ms = new MetaStep(name, prop)
546
590
  ms.setContext(currentObject)
547
591
  if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
@@ -600,6 +644,8 @@ function createSupportObjects(config) {
600
644
  let value
601
645
  if (container.sharedKeys.has(prop) && prop in container.support) {
602
646
  value = container.support[prop]
647
+ } else if (prop in container.support && typeof container.support[prop] === 'function') {
648
+ value = container.support[prop]
603
649
  } else {
604
650
  value = lazyLoad(prop)
605
651
  }
@@ -614,6 +660,9 @@ function createSupportObjects(config) {
614
660
  if (container.sharedKeys.has(key) && key in container.support) {
615
661
  return container.support[key]
616
662
  }
663
+ if (key in container.support && typeof container.support[key] === 'function') {
664
+ return container.support[key]
665
+ }
617
666
  return lazyLoad(key)
618
667
  },
619
668
  },
@@ -654,14 +703,28 @@ async function loadPluginFallback(modulePath, config) {
654
703
  async function createPlugins(config, options = {}) {
655
704
  const plugins = {}
656
705
 
657
- const enabledPluginsByOptions = (options.plugins || '').split(',')
706
+ const pluginOptionMap = new Map()
707
+ for (const token of (options.plugins || '').split(',').filter(Boolean)) {
708
+ const parts = token.split(':')
709
+ pluginOptionMap.set(parts[0], parts.slice(1))
710
+ }
711
+
712
+ for (const [name] of pluginOptionMap) {
713
+ if (!config[name]) config[name] = {}
714
+ }
715
+
658
716
  for (const pluginName in config) {
659
717
  if (!config[pluginName]) config[pluginName] = {}
660
718
  const pluginConfig = config[pluginName]
661
- if (!pluginConfig.enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
719
+ const enabledByCli = pluginOptionMap.has(pluginName)
720
+ if (!pluginConfig.enabled && !enabledByCli) {
662
721
  continue // plugin is disabled
663
722
  }
664
723
 
724
+ if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
725
+ pluginConfig._args = pluginOptionMap.get(pluginName)
726
+ }
727
+
665
728
  // Generic workers gate:
666
729
  // - runInWorker / runInWorkers controls plugin execution inside worker threads.
667
730
  // - runInParent / runInMain can disable plugin in workers parent process.
@@ -672,7 +735,7 @@ async function createPlugins(config, options = {}) {
672
735
  continue
673
736
  }
674
737
 
675
- if (!options.child && process.env.RUNS_WITH_WORKERS === 'true' && !runInParent) {
738
+ if (!options.child && store.workerMode && !runInParent) {
676
739
  continue
677
740
  }
678
741
  let module
@@ -681,7 +744,7 @@ async function createPlugins(config, options = {}) {
681
744
  module = pluginConfig.require
682
745
  if (module.startsWith('.')) {
683
746
  // local
684
- module = path.resolve(global.codecept_dir, module) // custom plugin
747
+ module = path.resolve(store.codeceptDir, module) // custom plugin
685
748
  }
686
749
  } else {
687
750
  module = `./plugin/${pluginName}.js`
@@ -698,12 +761,24 @@ async function createPlugins(config, options = {}) {
698
761
  }
699
762
 
700
763
  async function loadGherkinStepsAsync(paths) {
764
+ // Import BDD module to access step file tracking functions and step DSL
765
+ const bddModule = await import('./mocha/bdd.js')
766
+
701
767
  global.Before = fn => event.dispatcher.on(event.test.started, fn)
702
768
  global.After = fn => event.dispatcher.on(event.test.finished, fn)
703
769
  global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
704
770
 
705
- // Import BDD module to access step file tracking functions
706
- const bddModule = await import('./mocha/bdd.js')
771
+ // Scope-inject Given/When/Then/And while loading step files so they work
772
+ // with noGlobals: true. When noGlobals: false, globals.js has already set
773
+ // them as permanent globals — skip to avoid deleting them at the end.
774
+ const injectStepDsl = !!store.noGlobals
775
+ if (injectStepDsl) {
776
+ global.Given = bddModule.Given
777
+ global.When = bddModule.When
778
+ global.Then = bddModule.Then
779
+ global.And = bddModule.And
780
+ global.DefineParameterType = bddModule.defineParameterType
781
+ }
707
782
 
708
783
  // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
709
784
  // If gherkin.steps is Array, it will go the old way
@@ -716,7 +791,7 @@ async function loadGherkinStepsAsync(paths) {
716
791
  bddModule.clearCurrentStepFile()
717
792
  }
718
793
  } else {
719
- const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
794
+ const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
720
795
  if (folderPath !== '') {
721
796
  const files = globSync(folderPath)
722
797
  for (const file of files) {
@@ -731,6 +806,13 @@ async function loadGherkinStepsAsync(paths) {
731
806
  delete global.Before
732
807
  delete global.After
733
808
  delete global.Fail
809
+ if (injectStepDsl) {
810
+ delete global.Given
811
+ delete global.When
812
+ delete global.Then
813
+ delete global.And
814
+ delete global.DefineParameterType
815
+ }
734
816
  }
735
817
 
736
818
  function loadGherkinSteps(paths) {
@@ -764,7 +846,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
764
846
  }
765
847
  }
766
848
  if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
767
- modulePath = path.join(global.codecept_dir, modulePath)
849
+ modulePath = path.join(store.codeceptDir, modulePath)
768
850
  }
769
851
  try {
770
852
  // Use dynamic import for both ESM and CJS modules
@@ -888,7 +970,7 @@ async function loadTranslation(locale, vocabularies) {
888
970
  const langs = await Translation.getLangs()
889
971
  if (langs[locale]) {
890
972
  translation = new Translation(langs[locale])
891
- } else if (fileExists(path.join(global.codecept_dir, locale))) {
973
+ } else if (fileExists(path.join(store.codeceptDir, locale))) {
892
974
  // get from a provided file instead
893
975
  translation = Translation.createDefault()
894
976
  translation.loadVocabulary(locale)
@@ -905,7 +987,7 @@ function getHelperModuleName(helperName, config) {
905
987
  // classical require
906
988
  if (config[helperName].require) {
907
989
  if (config[helperName].require.startsWith('.')) {
908
- let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
990
+ let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
909
991
  // Add .js extension if not present for ESM compatibility
910
992
  if (!path.extname(helperPath)) {
911
993
  helperPath += '.js'
package/lib/effects.js CHANGED
@@ -4,6 +4,7 @@ import store from './store.js'
4
4
  import event from './event.js'
5
5
  import container from './container.js'
6
6
  import MetaStep from './step/meta.js'
7
+ import { empty } from './assert/empty.js'
7
8
  import { isAsyncFunction } from './utils.js'
8
9
 
9
10
  /**
@@ -111,6 +112,11 @@ class WithinStep extends MetaStep {
111
112
  *
112
113
  * @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
113
114
  */
115
+ let hopeThatFailures = []
116
+ event.dispatcher.on(event.test.before, () => {
117
+ hopeThatFailures = []
118
+ })
119
+
114
120
  async function hopeThat(callback) {
115
121
  if (store.dryRun) return
116
122
  const sessionName = 'hopeThat'
@@ -131,6 +137,7 @@ async function hopeThat(callback) {
131
137
  result = false
132
138
  const msg = err.inspect ? err.inspect() : err.toString()
133
139
  output.debug(`Unsuccessful assertion > ${msg}`)
140
+ hopeThatFailures.push(msg)
134
141
  event.dispatcher.once(event.test.finished, test => {
135
142
  if (!test.notes) test.notes = []
136
143
  test.notes.push({ type: 'conditionalError', text: msg })
@@ -153,6 +160,16 @@ async function hopeThat(callback) {
153
160
  )
154
161
  }
155
162
 
163
+ /**
164
+ * Asserts that no `hopeThat` soft assertion has failed in the current test.
165
+ * Call once at the end of a scenario to fail it when any soft assertion failed.
166
+ */
167
+ hopeThat.noErrors = function () {
168
+ const failures = hopeThatFailures
169
+ hopeThatFailures = []
170
+ empty('soft assertions').assert(failures)
171
+ }
172
+
156
173
  /**
157
174
  * A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
158
175
  *
@@ -82,6 +82,10 @@ class WebElement {
82
82
  async getProperty(name) {
83
83
  switch (this.helperType) {
84
84
  case 'playwright':
85
+ // For Locator objects, use inputValue() for the 'value' property
86
+ if (name === 'value' && this.element.inputValue) {
87
+ return this.element.inputValue()
88
+ }
85
89
  return this.element.evaluate((el, propName) => el[propName], name)
86
90
  case 'webdriver':
87
91
  return this.element.getProperty(name)
@@ -237,16 +241,149 @@ class WebElement {
237
241
  async type(text, options = {}) {
238
242
  switch (this.helperType) {
239
243
  case 'playwright':
244
+ // Playwright Locator objects use fill() instead of type()
245
+ if (this.element.fill) {
246
+ return this.element.fill(text, options)
247
+ }
240
248
  return this.element.type(text, options)
241
249
  case 'webdriver':
242
250
  return this.element.setValue(text)
243
251
  case 'puppeteer':
252
+ await this.element.evaluate(el => { el.value = '' })
244
253
  return this.element.type(text, options)
245
254
  default:
246
255
  throw new Error(`Unsupported helper type: ${this.helperType}`)
247
256
  }
248
257
  }
249
258
 
259
+ /**
260
+ * Run a function in the browser with this element as the first argument.
261
+ * @param {Function} fn Browser-side function. Receives the element, then extra args.
262
+ * @param {...any} args Additional arguments passed to the function
263
+ * @returns {Promise<any>} Value returned by fn
264
+ */
265
+ async evaluate(fn, ...args) {
266
+ switch (this.helperType) {
267
+ case 'playwright':
268
+ case 'puppeteer':
269
+ return this.element.evaluate(fn, ...args)
270
+ case 'webdriver':
271
+ return this.helper.executeScript(fn, this.element, ...args)
272
+ default:
273
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Focus the element.
279
+ * @returns {Promise<void>}
280
+ */
281
+ async focus() {
282
+ switch (this.helperType) {
283
+ case 'playwright':
284
+ return this.element.focus()
285
+ case 'puppeteer':
286
+ if (this.element.focus) return this.element.focus()
287
+ return this.element.evaluate(el => el.focus())
288
+ case 'webdriver':
289
+ return this.helper.executeScript(el => el.focus(), this.element)
290
+ default:
291
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Type characters via the page/browser keyboard into the focused element.
297
+ * Unlike `type()`, this does not call `.fill()`/`.setValue()`, so it works
298
+ * with contenteditable nodes, iframe bodies, and editor-owned hidden textareas.
299
+ * @param {string} text Text to send
300
+ * @param {Object} [options] Options (e.g. `{ delay }`)
301
+ * @returns {Promise<void>}
302
+ */
303
+ async typeText(text, options = {}) {
304
+ const s = String(text)
305
+ switch (this.helperType) {
306
+ case 'playwright':
307
+ case 'puppeteer':
308
+ return this.helper.page.keyboard.type(s, options)
309
+ case 'webdriver': {
310
+ const ENTER = '\uE007'
311
+ const parts = s.split('\n')
312
+ for (let i = 0; i < parts.length; i++) {
313
+ if (parts[i]) await this.helper.browser.keys(parts[i])
314
+ if (i < parts.length - 1) await this.helper.browser.keys(ENTER)
315
+ }
316
+ return
317
+ }
318
+ default:
319
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Select all content in the focused field and delete it via keyboard input.
325
+ * Sends Ctrl+A and Meta+A (so it works across platforms) followed by Backspace.
326
+ * @returns {Promise<void>}
327
+ */
328
+ async selectAllAndDelete() {
329
+ switch (this.helperType) {
330
+ case 'playwright':
331
+ await this.helper.page.keyboard.press('Control+a').catch(() => {})
332
+ await this.helper.page.keyboard.press('Meta+a').catch(() => {})
333
+ await this.helper.page.keyboard.press('Backspace')
334
+ return
335
+ case 'puppeteer':
336
+ for (const mod of ['Control', 'Meta']) {
337
+ try {
338
+ await this.helper.page.keyboard.down(mod)
339
+ await this.helper.page.keyboard.press('KeyA')
340
+ await this.helper.page.keyboard.up(mod)
341
+ } catch (e) {}
342
+ }
343
+ await this.helper.page.keyboard.press('Backspace')
344
+ return
345
+ case 'webdriver': {
346
+ const b = this.helper.browser
347
+ await b.keys(['Control', 'a']).catch(() => {})
348
+ await b.keys(['Meta', 'a']).catch(() => {})
349
+ await b.keys(['Backspace'])
350
+ return
351
+ }
352
+ default:
353
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Treat this element as an iframe; invoke `fn` with a WebElement wrapping
359
+ * the iframe body. For WebDriver this switches the browser into the frame
360
+ * for the duration of the callback and switches back on exit.
361
+ * @param {(body: WebElement) => Promise<any>} fn
362
+ * @returns {Promise<any>} Return value of fn
363
+ */
364
+ async inIframe(fn) {
365
+ switch (this.helperType) {
366
+ case 'playwright':
367
+ case 'puppeteer': {
368
+ const frame = await this.element.contentFrame()
369
+ const body = await frame.$('body')
370
+ return fn(new WebElement(body, this.helper))
371
+ }
372
+ case 'webdriver': {
373
+ const browser = this.helper.browser
374
+ await browser.switchFrame(this.element)
375
+ try {
376
+ const body = await browser.$('body')
377
+ return await fn(new WebElement(body, this.helper))
378
+ } finally {
379
+ await browser.switchFrame(null)
380
+ }
381
+ }
382
+ default:
383
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
384
+ }
385
+ }
386
+
250
387
  /**
251
388
  * Find first child element matching the locator
252
389
  * @param {string|Object} locator Element locator
@@ -257,7 +394,18 @@ class WebElement {
257
394
 
258
395
  switch (this.helperType) {
259
396
  case 'playwright':
260
- childElement = await this.element.$(this._normalizeLocator(locator))
397
+ // Playwright Locator objects use locator() method
398
+ if (this.element.locator) {
399
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
400
+ // Get the element handle from the locator
401
+ try {
402
+ childElement = await childLocator.elementHandle()
403
+ } catch (e) {
404
+ return null
405
+ }
406
+ } else {
407
+ childElement = await this.element.$(this._normalizeLocator(locator))
408
+ }
261
409
  break
262
410
  case 'webdriver':
263
411
  try {
@@ -286,7 +434,14 @@ class WebElement {
286
434
 
287
435
  switch (this.helperType) {
288
436
  case 'playwright':
289
- childElements = await this.element.$$(this._normalizeLocator(locator))
437
+ // Playwright Locator objects use locator() method
438
+ if (this.element.locator) {
439
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
440
+ // Get all element handles from the locator
441
+ childElements = await childLocator.elementHandles()
442
+ } else {
443
+ childElements = await this.element.$$(this._normalizeLocator(locator))
444
+ }
290
445
  break
291
446
  case 'webdriver':
292
447
  childElements = await this.element.$$(this._normalizeLocator(locator))
@@ -325,7 +480,7 @@ class WebElement {
325
480
  parts.unshift(`${tagName}${pathIndex}`)
326
481
  current = current.parentElement
327
482
  }
328
- return '/' + parts.join('/')
483
+ return '//' + parts.join('/')
329
484
  }
330
485
 
331
486
  switch (this.helperType) {
@@ -358,6 +513,43 @@ class WebElement {
358
513
  return simplifyHtmlElement(outerHTML, maxLength)
359
514
  }
360
515
 
516
+ /**
517
+ * Plain-object snapshot of the element — text, simplified HTML, visibility,
518
+ * enabled state, and a curated set of attributes. Each underlying call is
519
+ * isolated so a single failure (e.g. detached element) doesn't poison the
520
+ * rest. Suitable for JSON.stringify, log output, MCP tool responses.
521
+ *
522
+ * @param {object} [opts]
523
+ * @param {number} [opts.maxHtmlLength=300] passed through to toSimplifiedHTML
524
+ * @param {string[]} [opts.attrs] attribute names to surface
525
+ * @returns {Promise<{text?: string, html?: string, visible?: boolean, enabled?: boolean, attrs?: object}>}
526
+ */
527
+ async describe({ maxHtmlLength = 300, attrs = ['id', 'class', 'name', 'role', 'type', 'href', 'value', 'aria-label', 'placeholder', 'data-testid'] } = {}) {
528
+ const out = {}
529
+ await Promise.all([
530
+ this.toSimplifiedHTML(maxHtmlLength).then(v => { if (v) out.html = v }, () => {}),
531
+ this.getText().then(v => { const t = v?.trim(); if (t) out.text = t }, () => {}),
532
+ this.isVisible().then(v => { out.visible = v }, () => {}),
533
+ this.isEnabled().then(v => { out.enabled = v }, () => {}),
534
+ ])
535
+ const collected = {}
536
+ await Promise.all(attrs.map(async name => {
537
+ try {
538
+ const v = await this.getAttribute(name)
539
+ if (v != null && v !== '') collected[name] = v
540
+ } catch {}
541
+ }))
542
+ if (Object.keys(collected).length) out.attrs = collected
543
+ return out
544
+ }
545
+
546
+ // Make accidental JSON.stringify (e.g. returning a WebElement from MCP run_code)
547
+ // produce a usable hint instead of `{}` — the underlying handle isn't
548
+ // serializable. Use .describe() for the real plain-object snapshot.
549
+ toJSON() {
550
+ return `[WebElement ${this.helperType} — call .describe() for a plain-object snapshot or .toSimplifiedHTML() for HTML]`
551
+ }
552
+
361
553
  _normalizeLocator(locator) {
362
554
  if (typeof locator === 'string') {
363
555
  return locator