codeceptjs 4.0.2-beta.9 → 4.0.2

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 (326) 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 +246 -2
  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 +8 -8
  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/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. 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
@@ -81,6 +82,10 @@ class WebElement {
81
82
  async getProperty(name) {
82
83
  switch (this.helperType) {
83
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
+ }
84
89
  return this.element.evaluate((el, propName) => el[propName], name)
85
90
  case 'webdriver':
86
91
  return this.element.getProperty(name)
@@ -236,16 +241,149 @@ class WebElement {
236
241
  async type(text, options = {}) {
237
242
  switch (this.helperType) {
238
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
+ }
239
248
  return this.element.type(text, options)
240
249
  case 'webdriver':
241
250
  return this.element.setValue(text)
242
251
  case 'puppeteer':
252
+ await this.element.evaluate(el => { el.value = '' })
243
253
  return this.element.type(text, options)
244
254
  default:
245
255
  throw new Error(`Unsupported helper type: ${this.helperType}`)
246
256
  }
247
257
  }
248
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
+
249
387
  /**
250
388
  * Find first child element matching the locator
251
389
  * @param {string|Object} locator Element locator
@@ -256,7 +394,18 @@ class WebElement {
256
394
 
257
395
  switch (this.helperType) {
258
396
  case 'playwright':
259
- 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
+ }
260
409
  break
261
410
  case 'webdriver':
262
411
  try {
@@ -285,7 +434,14 @@ class WebElement {
285
434
 
286
435
  switch (this.helperType) {
287
436
  case 'playwright':
288
- 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
+ }
289
445
  break
290
446
  case 'webdriver':
291
447
  childElements = await this.element.$$(this._normalizeLocator(locator))
@@ -306,6 +462,94 @@ class WebElement {
306
462
  * @returns {string} Normalized CSS selector
307
463
  * @private
308
464
  */
465
+ async toAbsoluteXPath() {
466
+ const xpathFn = (el) => {
467
+ const parts = []
468
+ let current = el
469
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
470
+ let index = 0
471
+ let sibling = current.previousSibling
472
+ while (sibling) {
473
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
474
+ index++
475
+ }
476
+ sibling = sibling.previousSibling
477
+ }
478
+ const tagName = current.tagName.toLowerCase()
479
+ const pathIndex = index > 0 ? `[${index + 1}]` : ''
480
+ parts.unshift(`${tagName}${pathIndex}`)
481
+ current = current.parentElement
482
+ }
483
+ return '//' + parts.join('/')
484
+ }
485
+
486
+ switch (this.helperType) {
487
+ case 'playwright':
488
+ return this.element.evaluate(xpathFn)
489
+ case 'puppeteer':
490
+ return this.element.evaluate(xpathFn)
491
+ case 'webdriver':
492
+ return this.helper.browser.execute(xpathFn, this.element)
493
+ default:
494
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
495
+ }
496
+ }
497
+
498
+ async toOuterHTML() {
499
+ switch (this.helperType) {
500
+ case 'playwright':
501
+ return this.element.evaluate(el => el.outerHTML)
502
+ case 'puppeteer':
503
+ return this.element.evaluate(el => el.outerHTML)
504
+ case 'webdriver':
505
+ return this.helper.browser.execute(el => el.outerHTML, this.element)
506
+ default:
507
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
508
+ }
509
+ }
510
+
511
+ async toSimplifiedHTML(maxLength = 300) {
512
+ const outerHTML = await this.toOuterHTML()
513
+ return simplifyHtmlElement(outerHTML, maxLength)
514
+ }
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
+
309
553
  _normalizeLocator(locator) {
310
554
  if (typeof locator === 'string') {
311
555
  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)
@@ -1543,8 +1543,8 @@ class Appium extends Webdriver {
1543
1543
  /**
1544
1544
  * {{> dontSeeElement }}
1545
1545
  */
1546
- async dontSeeElement(locator) {
1547
- if (this.isWeb) return super.dontSeeElement(locator)
1546
+ async dontSeeElement(locator, context = null) {
1547
+ if (this.isWeb) return super.dontSeeElement(locator, context)
1548
1548
 
1549
1549
  // For mobile native apps, use safe isDisplayed wrapper
1550
1550
  const parsedLocator = parseLocator.call(this, locator)
@@ -1589,9 +1589,9 @@ class Appium extends Webdriver {
1589
1589
  * {{> fillField }}
1590
1590
  *
1591
1591
  */
1592
- async fillField(field, value) {
1592
+ async fillField(field, value, context = null) {
1593
1593
  value = value.toString()
1594
- if (this.isWeb) return super.fillField(field, value)
1594
+ if (this.isWeb) return super.fillField(field, value, context)
1595
1595
  return super.fillField(parseLocator.call(this, field), value)
1596
1596
  }
1597
1597
 
@@ -1706,8 +1706,8 @@ class Appium extends Webdriver {
1706
1706
  * {{> seeElement }}
1707
1707
  *
1708
1708
  */
1709
- async seeElement(locator) {
1710
- if (this.isWeb) return super.seeElement(locator)
1709
+ async seeElement(locator, context = null) {
1710
+ if (this.isWeb) return super.seeElement(locator, context)
1711
1711
 
1712
1712
  // For mobile native apps, use safe isDisplayed wrapper
1713
1713
  const parsedLocator = parseLocator.call(this, locator)
@@ -1754,8 +1754,8 @@ class Appium extends Webdriver {
1754
1754
  *
1755
1755
  * Supported only for web testing
1756
1756
  */
1757
- async selectOption(select, option) {
1758
- if (this.isWeb) return super.selectOption(select, option)
1757
+ async selectOption(select, option, context = null) {
1758
+ if (this.isWeb) return super.selectOption(select, option, context)
1759
1759
  throw new Error("Should be used only in Web context. In native context use 'click' method instead")
1760
1760
  }
1761
1761
 
@@ -4,6 +4,7 @@ import fs from 'fs'
4
4
 
5
5
  import Helper from '@codeceptjs/helper'
6
6
  import { fileExists } from '../utils.js'
7
+ import store from '../store.js'
7
8
  import { fileIncludes } from '../assert/include.js'
8
9
  import { fileEquals } from '../assert/equal.js'
9
10
 
@@ -33,7 +34,7 @@ import { fileEquals } from '../assert/equal.js'
33
34
  class FileSystem extends Helper {
34
35
  constructor() {
35
36
  super()
36
- this.dir = global.codecept_dir
37
+ this.dir = store.codeceptDir
37
38
  this.file = ''
38
39
  }
39
40
 
@@ -52,7 +53,7 @@ class FileSystem extends Helper {
52
53
  * @param {string} openPath
53
54
  */
54
55
  amInPath(openPath) {
55
- this.dir = path.join(global.codecept_dir, openPath)
56
+ this.dir = path.join(store.codeceptDir, openPath)
56
57
  try {
57
58
  this.debugSection('Dir', this.dir)
58
59
  } catch (e) {
@@ -2,6 +2,7 @@ import path from 'path'
2
2
 
3
3
  import HelperModule from '@codeceptjs/helper'
4
4
  import GraphQL from './GraphQL.js'
5
+ import store from '../store.js'
5
6
 
6
7
  /**
7
8
  * Helper for managing remote data using GraphQL queries.
@@ -251,7 +252,7 @@ class GraphQLDataFactory extends Helper {
251
252
  try {
252
253
  require.resolve(modulePath)
253
254
  } catch (e) {
254
- modulePath = path.join(global.codecept_dir, modulePath)
255
+ modulePath = path.join(store.codeceptDir, modulePath)
255
256
  }
256
257
  const builder = require(modulePath)
257
258
  return builder.build(data)