codeceptjs 4.0.2-beta.9 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +84 -41
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +253 -6
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +33 -12
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/asyncWrapper.js +2 -2
  151. package/lib/mocha/cli.js +19 -1
  152. package/lib/mocha/factory.js +11 -1
  153. package/lib/mocha/gherkin.js +5 -6
  154. package/lib/mocha/inject.js +1 -1
  155. package/lib/mocha/scenarioConfig.js +2 -1
  156. package/lib/mocha/ui.js +5 -6
  157. package/lib/parser.js +2 -2
  158. package/lib/pause.js +38 -4
  159. package/lib/plugin/aiTrace.js +457 -0
  160. package/lib/plugin/analyze.js +9 -9
  161. package/lib/plugin/auth.js +5 -4
  162. package/lib/plugin/browser.js +77 -0
  163. package/lib/plugin/expose.js +159 -0
  164. package/lib/plugin/heal.js +47 -3
  165. package/lib/plugin/junitReporter.js +303 -0
  166. package/lib/plugin/pageInfo.js +54 -52
  167. package/lib/plugin/pause.js +131 -0
  168. package/lib/plugin/pauseOnFail.js +11 -33
  169. package/lib/plugin/retryFailedStep.js +43 -32
  170. package/lib/plugin/screencast.js +289 -0
  171. package/lib/plugin/screenshot.js +558 -0
  172. package/lib/plugin/screenshotOnFail.js +9 -170
  173. package/lib/plugin/stepTimeout.js +3 -2
  174. package/lib/recorder.js +1 -1
  175. package/lib/rerun.js +2 -1
  176. package/lib/result.js +2 -1
  177. package/lib/step/base.js +10 -9
  178. package/lib/step/comment.js +2 -2
  179. package/lib/step/config.js +15 -2
  180. package/lib/step/helper.js +4 -4
  181. package/lib/step/meta.js +3 -3
  182. package/lib/step/record.js +5 -5
  183. package/lib/store.js +72 -3
  184. package/lib/translation.js +2 -1
  185. package/lib/utils/loaderCheck.js +28 -0
  186. package/lib/utils/mask_data.js +2 -1
  187. package/lib/utils/pluginParser.js +151 -0
  188. package/lib/utils/trace.js +297 -0
  189. package/lib/utils/typescript.js +188 -23
  190. package/lib/utils.js +77 -3
  191. package/lib/workers.js +65 -40
  192. package/package.json +37 -32
  193. package/typings/index.d.ts +119 -8
  194. package/typings/promiseBasedTypes.d.ts +3158 -6065
  195. package/typings/types.d.ts +3453 -6494
  196. package/docs/webapi/amOnPage.mustache +0 -11
  197. package/docs/webapi/appendField.mustache +0 -11
  198. package/docs/webapi/attachFile.mustache +0 -12
  199. package/docs/webapi/blur.mustache +0 -18
  200. package/docs/webapi/checkOption.mustache +0 -13
  201. package/docs/webapi/clearCookie.mustache +0 -9
  202. package/docs/webapi/clearField.mustache +0 -9
  203. package/docs/webapi/click.mustache +0 -29
  204. package/docs/webapi/clickLink.mustache +0 -8
  205. package/docs/webapi/closeCurrentTab.mustache +0 -7
  206. package/docs/webapi/closeOtherTabs.mustache +0 -8
  207. package/docs/webapi/dontSee.mustache +0 -11
  208. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  209. package/docs/webapi/dontSeeCookie.mustache +0 -8
  210. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  211. package/docs/webapi/dontSeeElement.mustache +0 -8
  212. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  213. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  214. package/docs/webapi/dontSeeInField.mustache +0 -11
  215. package/docs/webapi/dontSeeInSource.mustache +0 -8
  216. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  217. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  218. package/docs/webapi/doubleClick.mustache +0 -13
  219. package/docs/webapi/downloadFile.mustache +0 -12
  220. package/docs/webapi/dragAndDrop.mustache +0 -9
  221. package/docs/webapi/dragSlider.mustache +0 -11
  222. package/docs/webapi/executeAsyncScript.mustache +0 -24
  223. package/docs/webapi/executeScript.mustache +0 -26
  224. package/docs/webapi/fillField.mustache +0 -16
  225. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  226. package/docs/webapi/focus.mustache +0 -13
  227. package/docs/webapi/forceClick.mustache +0 -28
  228. package/docs/webapi/forceRightClick.mustache +0 -18
  229. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  230. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  231. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  232. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  233. package/docs/webapi/grabCookie.mustache +0 -11
  234. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  235. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  236. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  237. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  238. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  239. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  240. package/docs/webapi/grabGeoLocation.mustache +0 -8
  241. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  242. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  243. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  244. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  245. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  246. package/docs/webapi/grabPopupText.mustache +0 -5
  247. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  248. package/docs/webapi/grabSource.mustache +0 -8
  249. package/docs/webapi/grabTextFrom.mustache +0 -10
  250. package/docs/webapi/grabTextFromAll.mustache +0 -9
  251. package/docs/webapi/grabTitle.mustache +0 -8
  252. package/docs/webapi/grabValueFrom.mustache +0 -9
  253. package/docs/webapi/grabValueFromAll.mustache +0 -8
  254. package/docs/webapi/grabWebElement.mustache +0 -9
  255. package/docs/webapi/grabWebElements.mustache +0 -9
  256. package/docs/webapi/moveCursorTo.mustache +0 -12
  257. package/docs/webapi/openNewTab.mustache +0 -7
  258. package/docs/webapi/pressKey.mustache +0 -12
  259. package/docs/webapi/pressKeyDown.mustache +0 -12
  260. package/docs/webapi/pressKeyUp.mustache +0 -12
  261. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  262. package/docs/webapi/refreshPage.mustache +0 -6
  263. package/docs/webapi/resizeWindow.mustache +0 -6
  264. package/docs/webapi/rightClick.mustache +0 -14
  265. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  266. package/docs/webapi/saveScreenshot.mustache +0 -12
  267. package/docs/webapi/say.mustache +0 -10
  268. package/docs/webapi/scrollIntoView.mustache +0 -11
  269. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  270. package/docs/webapi/scrollPageToTop.mustache +0 -6
  271. package/docs/webapi/scrollTo.mustache +0 -12
  272. package/docs/webapi/see.mustache +0 -11
  273. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  274. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  275. package/docs/webapi/seeCookie.mustache +0 -8
  276. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  277. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  278. package/docs/webapi/seeElement.mustache +0 -8
  279. package/docs/webapi/seeElementInDOM.mustache +0 -8
  280. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  281. package/docs/webapi/seeInField.mustache +0 -12
  282. package/docs/webapi/seeInPopup.mustache +0 -8
  283. package/docs/webapi/seeInSource.mustache +0 -7
  284. package/docs/webapi/seeInTitle.mustache +0 -8
  285. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  286. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  287. package/docs/webapi/seeTextEquals.mustache +0 -9
  288. package/docs/webapi/seeTitleEquals.mustache +0 -8
  289. package/docs/webapi/seeTraffic.mustache +0 -36
  290. package/docs/webapi/selectOption.mustache +0 -21
  291. package/docs/webapi/setCookie.mustache +0 -16
  292. package/docs/webapi/setGeoLocation.mustache +0 -12
  293. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  294. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  295. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  296. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  297. package/docs/webapi/switchTo.mustache +0 -9
  298. package/docs/webapi/switchToNextTab.mustache +0 -10
  299. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  300. package/docs/webapi/type.mustache +0 -21
  301. package/docs/webapi/uncheckOption.mustache +0 -13
  302. package/docs/webapi/wait.mustache +0 -8
  303. package/docs/webapi/waitForClickable.mustache +0 -11
  304. package/docs/webapi/waitForCookie.mustache +0 -9
  305. package/docs/webapi/waitForDetached.mustache +0 -10
  306. package/docs/webapi/waitForDisabled.mustache +0 -6
  307. package/docs/webapi/waitForElement.mustache +0 -11
  308. package/docs/webapi/waitForEnabled.mustache +0 -6
  309. package/docs/webapi/waitForFunction.mustache +0 -17
  310. package/docs/webapi/waitForInvisible.mustache +0 -10
  311. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  312. package/docs/webapi/waitForText.mustache +0 -13
  313. package/docs/webapi/waitForValue.mustache +0 -10
  314. package/docs/webapi/waitForVisible.mustache +0 -10
  315. package/docs/webapi/waitInUrl.mustache +0 -9
  316. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  317. package/docs/webapi/waitToHide.mustache +0 -10
  318. package/docs/webapi/waitUrlEquals.mustache +0 -10
  319. package/lib/helper/AI.js +0 -214
  320. package/lib/helper/Mochawesome.js +0 -96
  321. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  322. package/lib/helper/extras/React.js +0 -65
  323. package/lib/listener/enhancedGlobalRetry.js +0 -110
  324. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  325. package/lib/plugin/htmlReporter.js +0 -3648
  326. package/lib/plugin/stepByStepReport.js +0 -427
  327. package/lib/plugin/subtitles.js +0 -89
  328. package/lib/retryCoordinator.js +0 -207
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
  *
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import { simplifyHtmlElement } from '../html.js'
2
3
 
3
4
  /**
4
5
  * Unified WebElement class that wraps native element instances from different helpers
@@ -14,10 +15,13 @@ class WebElement {
14
15
  _detectHelperType(helper) {
15
16
  if (!helper) return 'unknown'
16
17
 
17
- const className = helper.constructor.name
18
- if (className === 'Playwright') return 'playwright'
19
- if (className === 'WebDriver') return 'webdriver'
20
- if (className === 'Puppeteer') return 'puppeteer'
18
+ let ctor = helper.constructor
19
+ while (ctor && ctor.name) {
20
+ if (ctor.name === 'Playwright') return 'playwright'
21
+ if (ctor.name === 'WebDriver') return 'webdriver'
22
+ if (ctor.name === 'Puppeteer') return 'puppeteer'
23
+ ctor = Object.getPrototypeOf(ctor)
24
+ }
21
25
 
22
26
  return 'unknown'
23
27
  }
@@ -81,6 +85,10 @@ class WebElement {
81
85
  async getProperty(name) {
82
86
  switch (this.helperType) {
83
87
  case 'playwright':
88
+ // For Locator objects, use inputValue() for the 'value' property
89
+ if (name === 'value' && this.element.inputValue) {
90
+ return this.element.inputValue()
91
+ }
84
92
  return this.element.evaluate((el, propName) => el[propName], name)
85
93
  case 'webdriver':
86
94
  return this.element.getProperty(name)
@@ -236,16 +244,149 @@ class WebElement {
236
244
  async type(text, options = {}) {
237
245
  switch (this.helperType) {
238
246
  case 'playwright':
247
+ // Playwright Locator objects use fill() instead of type()
248
+ if (this.element.fill) {
249
+ return this.element.fill(text, options)
250
+ }
239
251
  return this.element.type(text, options)
240
252
  case 'webdriver':
241
253
  return this.element.setValue(text)
242
254
  case 'puppeteer':
255
+ await this.element.evaluate(el => { el.value = '' })
243
256
  return this.element.type(text, options)
244
257
  default:
245
258
  throw new Error(`Unsupported helper type: ${this.helperType}`)
246
259
  }
247
260
  }
248
261
 
262
+ /**
263
+ * Run a function in the browser with this element as the first argument.
264
+ * @param {Function} fn Browser-side function. Receives the element, then extra args.
265
+ * @param {...any} args Additional arguments passed to the function
266
+ * @returns {Promise<any>} Value returned by fn
267
+ */
268
+ async evaluate(fn, ...args) {
269
+ switch (this.helperType) {
270
+ case 'playwright':
271
+ case 'puppeteer':
272
+ return this.element.evaluate(fn, ...args)
273
+ case 'webdriver':
274
+ return this.helper.executeScript(fn, this.element, ...args)
275
+ default:
276
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Focus the element.
282
+ * @returns {Promise<void>}
283
+ */
284
+ async focus() {
285
+ switch (this.helperType) {
286
+ case 'playwright':
287
+ return this.element.focus()
288
+ case 'puppeteer':
289
+ if (this.element.focus) return this.element.focus()
290
+ return this.element.evaluate(el => el.focus())
291
+ case 'webdriver':
292
+ return this.helper.executeScript(el => el.focus(), this.element)
293
+ default:
294
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Type characters via the page/browser keyboard into the focused element.
300
+ * Unlike `type()`, this does not call `.fill()`/`.setValue()`, so it works
301
+ * with contenteditable nodes, iframe bodies, and editor-owned hidden textareas.
302
+ * @param {string} text Text to send
303
+ * @param {Object} [options] Options (e.g. `{ delay }`)
304
+ * @returns {Promise<void>}
305
+ */
306
+ async typeText(text, options = {}) {
307
+ const s = String(text)
308
+ switch (this.helperType) {
309
+ case 'playwright':
310
+ case 'puppeteer':
311
+ return this.helper.page.keyboard.type(s, options)
312
+ case 'webdriver': {
313
+ const ENTER = '\uE007'
314
+ const parts = s.split('\n')
315
+ for (let i = 0; i < parts.length; i++) {
316
+ if (parts[i]) await this.helper.browser.keys(parts[i])
317
+ if (i < parts.length - 1) await this.helper.browser.keys(ENTER)
318
+ }
319
+ return
320
+ }
321
+ default:
322
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Select all content in the focused field and delete it via keyboard input.
328
+ * Sends Ctrl+A and Meta+A (so it works across platforms) followed by Backspace.
329
+ * @returns {Promise<void>}
330
+ */
331
+ async selectAllAndDelete() {
332
+ switch (this.helperType) {
333
+ case 'playwright':
334
+ await this.helper.page.keyboard.press('Control+a').catch(() => {})
335
+ await this.helper.page.keyboard.press('Meta+a').catch(() => {})
336
+ await this.helper.page.keyboard.press('Backspace')
337
+ return
338
+ case 'puppeteer':
339
+ for (const mod of ['Control', 'Meta']) {
340
+ try {
341
+ await this.helper.page.keyboard.down(mod)
342
+ await this.helper.page.keyboard.press('KeyA')
343
+ await this.helper.page.keyboard.up(mod)
344
+ } catch (e) {}
345
+ }
346
+ await this.helper.page.keyboard.press('Backspace')
347
+ return
348
+ case 'webdriver': {
349
+ const b = this.helper.browser
350
+ await b.keys(['Control', 'a']).catch(() => {})
351
+ await b.keys(['Meta', 'a']).catch(() => {})
352
+ await b.keys(['Backspace'])
353
+ return
354
+ }
355
+ default:
356
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Treat this element as an iframe; invoke `fn` with a WebElement wrapping
362
+ * the iframe body. For WebDriver this switches the browser into the frame
363
+ * for the duration of the callback and switches back on exit.
364
+ * @param {(body: WebElement) => Promise<any>} fn
365
+ * @returns {Promise<any>} Return value of fn
366
+ */
367
+ async inIframe(fn) {
368
+ switch (this.helperType) {
369
+ case 'playwright':
370
+ case 'puppeteer': {
371
+ const frame = await this.element.contentFrame()
372
+ const body = await frame.$('body')
373
+ return fn(new WebElement(body, this.helper))
374
+ }
375
+ case 'webdriver': {
376
+ const browser = this.helper.browser
377
+ await browser.switchFrame(this.element)
378
+ try {
379
+ const body = await browser.$('body')
380
+ return await fn(new WebElement(body, this.helper))
381
+ } finally {
382
+ await browser.switchFrame(null)
383
+ }
384
+ }
385
+ default:
386
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
387
+ }
388
+ }
389
+
249
390
  /**
250
391
  * Find first child element matching the locator
251
392
  * @param {string|Object} locator Element locator
@@ -256,7 +397,18 @@ class WebElement {
256
397
 
257
398
  switch (this.helperType) {
258
399
  case 'playwright':
259
- childElement = await this.element.$(this._normalizeLocator(locator))
400
+ // Playwright Locator objects use locator() method
401
+ if (this.element.locator) {
402
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
403
+ // Get the element handle from the locator
404
+ try {
405
+ childElement = await childLocator.elementHandle()
406
+ } catch (e) {
407
+ return null
408
+ }
409
+ } else {
410
+ childElement = await this.element.$(this._normalizeLocator(locator))
411
+ }
260
412
  break
261
413
  case 'webdriver':
262
414
  try {
@@ -285,7 +437,14 @@ class WebElement {
285
437
 
286
438
  switch (this.helperType) {
287
439
  case 'playwright':
288
- childElements = await this.element.$$(this._normalizeLocator(locator))
440
+ // Playwright Locator objects use locator() method
441
+ if (this.element.locator) {
442
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
443
+ // Get all element handles from the locator
444
+ childElements = await childLocator.elementHandles()
445
+ } else {
446
+ childElements = await this.element.$$(this._normalizeLocator(locator))
447
+ }
289
448
  break
290
449
  case 'webdriver':
291
450
  childElements = await this.element.$$(this._normalizeLocator(locator))
@@ -306,6 +465,94 @@ class WebElement {
306
465
  * @returns {string} Normalized CSS selector
307
466
  * @private
308
467
  */
468
+ async toAbsoluteXPath() {
469
+ const xpathFn = (el) => {
470
+ const parts = []
471
+ let current = el
472
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
473
+ let index = 0
474
+ let sibling = current.previousSibling
475
+ while (sibling) {
476
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
477
+ index++
478
+ }
479
+ sibling = sibling.previousSibling
480
+ }
481
+ const tagName = current.tagName.toLowerCase()
482
+ const pathIndex = index > 0 ? `[${index + 1}]` : ''
483
+ parts.unshift(`${tagName}${pathIndex}`)
484
+ current = current.parentElement
485
+ }
486
+ return '//' + parts.join('/')
487
+ }
488
+
489
+ switch (this.helperType) {
490
+ case 'playwright':
491
+ return this.element.evaluate(xpathFn)
492
+ case 'puppeteer':
493
+ return this.element.evaluate(xpathFn)
494
+ case 'webdriver':
495
+ return this.helper.browser.execute(xpathFn, this.element)
496
+ default:
497
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
498
+ }
499
+ }
500
+
501
+ async toOuterHTML() {
502
+ switch (this.helperType) {
503
+ case 'playwright':
504
+ return this.element.evaluate(el => el.outerHTML)
505
+ case 'puppeteer':
506
+ return this.element.evaluate(el => el.outerHTML)
507
+ case 'webdriver':
508
+ return this.helper.browser.execute(el => el.outerHTML, this.element)
509
+ default:
510
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
511
+ }
512
+ }
513
+
514
+ async toSimplifiedHTML(maxLength = 300) {
515
+ const outerHTML = await this.toOuterHTML()
516
+ return simplifyHtmlElement(outerHTML, maxLength)
517
+ }
518
+
519
+ /**
520
+ * Plain-object snapshot of the element — text, simplified HTML, visibility,
521
+ * enabled state, and a curated set of attributes. Each underlying call is
522
+ * isolated so a single failure (e.g. detached element) doesn't poison the
523
+ * rest. Suitable for JSON.stringify, log output, MCP tool responses.
524
+ *
525
+ * @param {object} [opts]
526
+ * @param {number} [opts.maxHtmlLength=300] passed through to toSimplifiedHTML
527
+ * @param {string[]} [opts.attrs] attribute names to surface
528
+ * @returns {Promise<{text?: string, html?: string, visible?: boolean, enabled?: boolean, attrs?: object}>}
529
+ */
530
+ async describe({ maxHtmlLength = 300, attrs = ['id', 'class', 'name', 'role', 'type', 'href', 'value', 'aria-label', 'placeholder', 'data-testid'] } = {}) {
531
+ const out = {}
532
+ await Promise.all([
533
+ this.toSimplifiedHTML(maxHtmlLength).then(v => { if (v) out.html = v }, () => {}),
534
+ this.getText().then(v => { const t = v?.trim(); if (t) out.text = t }, () => {}),
535
+ this.isVisible().then(v => { out.visible = v }, () => {}),
536
+ this.isEnabled().then(v => { out.enabled = v }, () => {}),
537
+ ])
538
+ const collected = {}
539
+ await Promise.all(attrs.map(async name => {
540
+ try {
541
+ const v = await this.getAttribute(name)
542
+ if (v != null && v !== '') collected[name] = v
543
+ } catch {}
544
+ }))
545
+ if (Object.keys(collected).length) out.attrs = collected
546
+ return out
547
+ }
548
+
549
+ // Make accidental JSON.stringify (e.g. returning a WebElement from MCP run_code)
550
+ // produce a usable hint instead of `{}` — the underlying handle isn't
551
+ // serializable. Use .describe() for the real plain-object snapshot.
552
+ toJSON() {
553
+ return `[WebElement ${this.helperType} — call .describe() for a plain-object snapshot or .toSimplifiedHTML() for HTML]`
554
+ }
555
+
309
556
  _normalizeLocator(locator) {
310
557
  if (typeof locator === 'string') {
311
558
  return locator
package/lib/els.js CHANGED
@@ -6,10 +6,11 @@ import recordStep from './step/record.js'
6
6
  import FuncStep from './step/func.js'
7
7
  import { truth } from './assert/truth.js'
8
8
  import { isAsyncFunction, humanizeFunction } from './utils.js'
9
+ import WebElement from './element/WebElement.js'
9
10
 
10
11
  function element(purpose, locator, fn) {
11
12
  let stepConfig
12
- if (arguments[arguments.length - 1] instanceof StepConfig) {
13
+ if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
13
14
  stepConfig = arguments[arguments.length - 1]
14
15
  }
15
16
 
@@ -28,7 +29,8 @@ function element(purpose, locator, fn) {
28
29
  const els = await step.helper._locate(locator)
29
30
  output.debug(`Found ${els.length} elements, using first element`)
30
31
 
31
- return fn(els[0])
32
+ const wrapped = new WebElement(els[0], step.helper)
33
+ return fn(wrapped)
32
34
  },
33
35
  stepConfig,
34
36
  )
@@ -52,7 +54,8 @@ function eachElement(purpose, locator, fn) {
52
54
  let i = 0
53
55
  for (const el of els) {
54
56
  try {
55
- await fn(el, i)
57
+ const wrapped = new WebElement(el, step.helper)
58
+ await fn(wrapped, i)
56
59
  } catch (err) {
57
60
  output.error(`eachElement: failed operation on element #${i} ${el}`)
58
61
  errs.push(err)
@@ -74,7 +77,8 @@ function expectElement(locator, fn) {
74
77
  const els = await step.helper._locate(locator)
75
78
  output.debug(`Found ${els.length} elements, first will be used for assertion`)
76
79
 
77
- const result = await fn(els[0])
80
+ const wrapped = new WebElement(els[0], step.helper)
81
+ const result = await fn(wrapped)
78
82
  const assertion = truth(`element (${locator})`, fn.toString())
79
83
  assertion.assert(result)
80
84
  })
@@ -92,7 +96,8 @@ function expectAnyElement(locator, fn) {
92
96
 
93
97
  let found = false
94
98
  for (const el of els) {
95
- const result = await fn(el)
99
+ const wrapped = new WebElement(el, step.helper)
100
+ const result = await fn(wrapped)
96
101
  if (result) {
97
102
  found = true
98
103
  break
@@ -113,7 +118,8 @@ function expectAllElements(locator, fn) {
113
118
  let i = 1
114
119
  for (const el of els) {
115
120
  output.debug(`checking element #${i}: ${el}`)
116
- const result = await fn(el)
121
+ const wrapped = new WebElement(el, step.helper)
122
+ const result = await fn(wrapped)
117
123
  const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
118
124
  assertion.assert(result)
119
125
  i++
package/lib/globals.js CHANGED
@@ -8,27 +8,47 @@ import fsPath from 'path'
8
8
  import ActorFactory from './actor.js'
9
9
  import output from './output.js'
10
10
  import locator from './locator.js'
11
+ import store from './store.js'
11
12
 
12
13
  /**
13
14
  * Initialize CodeceptJS core globals
14
15
  * Called from Codecept.initGlobals()
15
16
  */
16
17
  export async function initCodeceptGlobals(dir, config, container) {
18
+ store.initialize({
19
+ codeceptDir: dir,
20
+ outputDir: fsPath.resolve(dir, config.output),
21
+ })
22
+
23
+ store.noGlobals = config.noGlobals || false
24
+ store.maskSensitiveData = config.maskSensitiveData || false
25
+
26
+ // Keep globals for backward compat with external plugins
17
27
  global.codecept_dir = dir
18
28
  global.output_dir = fsPath.resolve(dir, config.output)
19
29
 
20
- if (config.noGlobals) return;
21
- // Set up actor global - will use container when available
22
- global.actor = global.codecept_actor = (obj) => {
23
- return ActorFactory(obj, global.container || container)
24
- }
25
- global.Actor = global.actor
26
-
27
- // Use dynamic imports for modules to avoid circular dependencies
30
+ // pause/inject/share stay global even under noGlobals — they're the everyday
31
+ // debugging/wiring entry points and have no useful import alternative for
32
+ // page-object code that runs before the container is available.
28
33
  global.pause = async (...args) => {
29
34
  const pauseModule = await import('./pause.js')
30
35
  return (pauseModule.default || pauseModule)(...args)
31
36
  }
37
+ global.inject = () => container.support()
38
+ global.share = container.share
39
+
40
+ if (config.noGlobals) return;
41
+
42
+ output.print(output.styles.debug('Global functions are deprecated. Use `import { Helper, within, session } from "codeceptjs"` instead. Set `noGlobals: true` in config to disable globals.'));
43
+
44
+ const HelperModule = await import('@codeceptjs/helper')
45
+ global.Helper = global.codecept_helper = HelperModule.default || HelperModule
46
+
47
+ // Set up actor global - will use container when available
48
+ global.actor = global.codecept_actor = (obj) => {
49
+ return ActorFactory(obj, container)
50
+ }
51
+ global.Actor = global.actor
32
52
 
33
53
  global.within = async (...args) => {
34
54
  return (await import('./effects.js')).within(...args)
@@ -46,14 +66,9 @@ export async function initCodeceptGlobals(dir, config, container) {
46
66
  return locator.build(locatorQuery)
47
67
  }
48
68
 
49
- global.inject = () => container.support()
50
- global.share = container.share
51
-
52
69
  const secretModule = await import('./secret.js')
53
70
  global.secret = secretModule.secret || (secretModule.default && secretModule.default.secret)
54
71
 
55
- global.codecept_debug = output.debug
56
-
57
72
  const codeceptjsModule = await import('./index.js') // load all objects
58
73
  global.codeceptjs = codeceptjsModule.default || codeceptjsModule
59
74
 
@@ -65,9 +80,6 @@ export async function initCodeceptGlobals(dir, config, container) {
65
80
  global.Then = stepDefinitions.Then
66
81
  global.DefineParameterType = stepDefinitions.defineParameterType
67
82
 
68
- // debug mode
69
- global.debugMode = false
70
-
71
83
  // mask sensitive data
72
84
  global.maskSensitiveData = config.maskSensitiveData || false
73
85
 
@@ -78,6 +90,8 @@ export async function initCodeceptGlobals(dir, config, container) {
78
90
  * Called from mocha/ui.js pre-require event
79
91
  */
80
92
  export function initMochaGlobals(context) {
93
+ if (store.noGlobals) return;
94
+
81
95
  // Mocha test framework globals
82
96
  global.BeforeAll = context.BeforeAll
83
97
  global.AfterAll = context.AfterAll
@@ -106,6 +120,8 @@ export function getGlobalNames() {
106
120
  return [
107
121
  'codecept_dir',
108
122
  'output_dir',
123
+ 'Helper',
124
+ 'codecept_helper',
109
125
  'actor',
110
126
  'codecept_actor',
111
127
  'Actor',
@@ -117,13 +133,11 @@ export function getGlobalNames() {
117
133
  'inject',
118
134
  'share',
119
135
  'secret',
120
- 'codecept_debug',
121
136
  'codeceptjs',
122
137
  'Given',
123
138
  'When',
124
139
  'Then',
125
140
  'DefineParameterType',
126
- 'debugMode',
127
141
  'maskSensitiveData',
128
142
  'BeforeAll',
129
143
  'AfterAll',
@@ -136,6 +150,5 @@ export function getGlobalNames() {
136
150
  'After',
137
151
  'Scenario',
138
152
  'xScenario',
139
- 'container'
140
153
  ]
141
154
  }
package/lib/heal.js CHANGED
@@ -4,6 +4,7 @@ import colors from 'chalk'
4
4
  import recorder from './recorder.js'
5
5
  import output from './output.js'
6
6
  import event from './event.js'
7
+ import container from './container.js'
7
8
 
8
9
  /**
9
10
  * @class
@@ -48,12 +49,14 @@ class Heal {
48
49
  }
49
50
 
50
51
  hasCorrespondingRecipes(step) {
51
- return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
52
+ return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.title)).length > 0
52
53
  }
53
54
 
54
55
  async getCodeSuggestions(context) {
55
56
  const suggestions = []
57
+ const stepName = context.step?.title
56
58
  const recipes = matchRecipes(this.recipes, this.contextName)
59
+ .filter(r => !r.steps || !stepName || r.steps.includes(stepName))
57
60
 
58
61
  debug('Recipes', recipes)
59
62
 
@@ -69,7 +72,7 @@ class Heal {
69
72
  if (!prepareFn) continue
70
73
 
71
74
  if (context[property]) continue
72
- context[property] = await prepareFn(global.inject())
75
+ context[property] = await prepareFn(container.support())
73
76
  }
74
77
 
75
78
  output.level(currentOutputLevel)
@@ -116,10 +119,10 @@ class Heal {
116
119
  })
117
120
 
118
121
  if (typeof codeSnippet === 'string') {
119
- const I = global.container.support('I')
122
+ const I = container.support('I')
120
123
  await eval(codeSnippet)
121
124
  } else if (typeof codeSnippet === 'function') {
122
- await codeSnippet(global.container.support())
125
+ await codeSnippet(container.support())
123
126
  }
124
127
 
125
128
  this.fixes.push({
@@ -1,6 +1,7 @@
1
1
  import path from 'path'
2
2
  import Helper from '@codeceptjs/helper'
3
3
  import REST from './REST.js'
4
+ import store from '../store.js'
4
5
 
5
6
  /**
6
7
  * Helper for managing remote data using REST API.
@@ -324,7 +325,7 @@ class ApiDataFactory extends Helper {
324
325
  await import.meta.resolve(modulePath)
325
326
  } catch (e) {
326
327
  // If not found, try relative to codecept_dir
327
- modulePath = path.join(global.codecept_dir, modulePath)
328
+ modulePath = path.join(store.codeceptDir, modulePath)
328
329
  }
329
330
  // check if the new syntax `export default new Factory()` is used and loads the builder, otherwise loads the module that used old syntax `module.exports = new Factory()`.
330
331
  const module = await import(modulePath)