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
@@ -10,6 +10,7 @@ import promiseRetry from 'promise-retry'
10
10
  import { includes as stringIncludes } from '../assert/include.js'
11
11
  import { urlEquals, equals } from '../assert/equal.js'
12
12
  import store from '../store.js'
13
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
13
14
  import output from '../output.js'
14
15
  const { debug } = output
15
16
  import { empty } from '../assert/empty.js'
@@ -40,6 +41,8 @@ import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElem
40
41
  import { dropFile } from './scripts/dropFile.js'
41
42
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
42
43
  import WebElement from '../element/WebElement.js'
44
+ import { selectElement } from './extras/elementSelection.js'
45
+ import { fillRichEditor } from './extras/richTextEditor.js'
43
46
 
44
47
  const SHADOW = 'shadow'
45
48
  const webRoot = 'body'
@@ -94,11 +97,7 @@ const config = {}
94
97
  * WebDriver helper which wraps [webdriverio](http://webdriver.io/) library to
95
98
  * manipulate browser using Selenium WebDriver or PhantomJS.
96
99
  *
97
- * WebDriver requires Selenium Server and ChromeDriver/GeckoDriver to be installed. Those tools can be easily installed via NPM. Please check [Testing with WebDriver](https://codecept.io/webdriver/#testing-with-webdriver) for more details.
98
- *
99
- * With the release of WebdriverIO version v8.14.0, and onwards, all driver management hassles are now a thing of the past 🙌. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/).
100
- * One of the significant advantages of this update is that you can now get rid of any driver services you previously had to manage, such as
101
- * `wdio-chromedriver-service`, `wdio-geckodriver-service`, `wdio-edgedriver-service`, `wdio-safaridriver-service`, and even `@wdio/selenium-standalone-service`.
100
+ * No Selenium Server, ChromeDriver, or GeckoDriver to install or start. Since WebdriverIO 9, driver management is fully automatic — WebdriverIO downloads and starts the matching driver for you. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/). Please check [Testing with WebDriver](https://codecept.io/webdriver/#testing-with-webdriver) for more details.
102
101
  *
103
102
  * For those who require custom driver options, fear not; WebDriver Helper allows you to pass in driver options through custom WebDriver configuration.
104
103
  * If you have a custom grid, use a cloud service, or prefer to run your own driver, there's no need to worry since WebDriver Helper will only start a driver when there are no other connection information settings like hostname or port specified.
@@ -905,13 +904,6 @@ class WebDriver extends Helper {
905
904
  return els
906
905
  }
907
906
 
908
- // special locator type for React
909
- if (locator.react) {
910
- const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined)
911
- this.debugSection('Elements', `Found ${els.length} react components`)
912
- return els
913
- }
914
-
915
907
  // special locator type for ARIA roles
916
908
  if (locator.role) {
917
909
  return this._locateByRole(locator)
@@ -1081,7 +1073,6 @@ class WebDriver extends Helper {
1081
1073
  /**
1082
1074
  * {{> click }}
1083
1075
  *
1084
- * {{ react }}
1085
1076
  */
1086
1077
  async click(locator, context = null) {
1087
1078
  const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
@@ -1093,8 +1084,7 @@ class WebDriver extends Helper {
1093
1084
  } else {
1094
1085
  assertElementExists(res, locator, 'Clickable element')
1095
1086
  }
1096
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1097
- const elem = usingFirstElement(res)
1087
+ const elem = selectElement(res, locator, this)
1098
1088
  highlightActiveElement.call(this, elem)
1099
1089
  return this.browser[clickMethod](getElementId(elem))
1100
1090
  }
@@ -1102,7 +1092,6 @@ class WebDriver extends Helper {
1102
1092
  /**
1103
1093
  * {{> forceClick }}
1104
1094
  *
1105
- * {{ react }}
1106
1095
  */
1107
1096
  async forceClick(locator, context = null) {
1108
1097
  const locateFn = prepareLocateFn.call(this, context)
@@ -1113,8 +1102,7 @@ class WebDriver extends Helper {
1113
1102
  } else {
1114
1103
  assertElementExists(res, locator, 'Clickable element')
1115
1104
  }
1116
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1117
- const elem = usingFirstElement(res)
1105
+ const elem = selectElement(res, locator, this)
1118
1106
  highlightActiveElement.call(this, elem)
1119
1107
 
1120
1108
  return this.executeScript(el => {
@@ -1130,7 +1118,6 @@ class WebDriver extends Helper {
1130
1118
  /**
1131
1119
  * {{> doubleClick }}
1132
1120
  *
1133
- * {{ react }}
1134
1121
  */
1135
1122
  async doubleClick(locator, context = null) {
1136
1123
  const locateFn = prepareLocateFn.call(this, context)
@@ -1141,9 +1128,8 @@ class WebDriver extends Helper {
1141
1128
  } else {
1142
1129
  assertElementExists(res, locator, 'Clickable element')
1143
1130
  }
1144
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1145
1131
 
1146
- const elem = usingFirstElement(res)
1132
+ const elem = selectElement(res, locator, this)
1147
1133
  highlightActiveElement.call(this, elem)
1148
1134
  return elem.doubleClick()
1149
1135
  }
@@ -1151,7 +1137,6 @@ class WebDriver extends Helper {
1151
1137
  /**
1152
1138
  * {{> rightClick }}
1153
1139
  *
1154
- * {{ react }}
1155
1140
  */
1156
1141
  async rightClick(locator, context) {
1157
1142
  const locateFn = prepareLocateFn.call(this, context)
@@ -1162,9 +1147,8 @@ class WebDriver extends Helper {
1162
1147
  } else {
1163
1148
  assertElementExists(res, locator, 'Clickable element')
1164
1149
  }
1165
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1166
1150
 
1167
- const el = usingFirstElement(res)
1151
+ const el = selectElement(res, locator, this)
1168
1152
 
1169
1153
  await el.moveTo()
1170
1154
 
@@ -1247,7 +1231,6 @@ class WebDriver extends Helper {
1247
1231
  /**
1248
1232
  * {{> forceRightClick }}
1249
1233
  *
1250
- * {{ react }}
1251
1234
  */
1252
1235
  async forceRightClick(locator, context = null) {
1253
1236
  const locateFn = prepareLocateFn.call(this, context)
@@ -1272,16 +1255,19 @@ class WebDriver extends Helper {
1272
1255
 
1273
1256
  /**
1274
1257
  * {{> fillField }}
1275
- * {{ react }}
1276
1258
  * {{ custom }}
1277
1259
  *
1278
1260
  */
1279
1261
  async fillField(field, value, context = null) {
1280
1262
  const res = await findFields.call(this, field, context)
1281
1263
  assertElementExists(res, field, 'Field')
1282
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1283
- const elem = usingFirstElement(res)
1264
+ const elem = selectElement(res, field, this)
1284
1265
  highlightActiveElement.call(this, elem)
1266
+
1267
+ if (await fillRichEditor(this, elem, value)) {
1268
+ return
1269
+ }
1270
+
1285
1271
  try {
1286
1272
  await elem.clearValue()
1287
1273
  } catch (err) {
@@ -1298,13 +1284,11 @@ class WebDriver extends Helper {
1298
1284
 
1299
1285
  /**
1300
1286
  * {{> appendField }}
1301
- * {{ react }}
1302
1287
  */
1303
1288
  async appendField(field, value, context = null) {
1304
1289
  const res = await findFields.call(this, field, context)
1305
1290
  assertElementExists(res, field, 'Field')
1306
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1307
- const elem = usingFirstElement(res)
1291
+ const elem = selectElement(res, field, this)
1308
1292
  highlightActiveElement.call(this, elem)
1309
1293
  return elem.addValue(value.toString())
1310
1294
  }
@@ -1316,8 +1300,7 @@ class WebDriver extends Helper {
1316
1300
  async clearField(field, context = null) {
1317
1301
  const res = await findFields.call(this, field, context)
1318
1302
  assertElementExists(res, field, 'Field')
1319
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1320
- const elem = usingFirstElement(res)
1303
+ const elem = selectElement(res, field, this)
1321
1304
  highlightActiveElement.call(this, elem)
1322
1305
  return elem.clearValue(getElementId(elem))
1323
1306
  }
@@ -1334,22 +1317,22 @@ class WebDriver extends Helper {
1334
1317
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1335
1318
  const els = await locateFn(select)
1336
1319
  assertElementExists(els, select, 'Selectable element')
1337
- return proceedSelectOption.call(this, usingFirstElement(els), option)
1320
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1338
1321
  }
1339
1322
 
1340
1323
  // Fuzzy: try combobox
1341
1324
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1342
1325
  let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1343
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1326
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1344
1327
 
1345
1328
  // Fuzzy: try listbox
1346
1329
  els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1347
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1330
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1348
1331
 
1349
1332
  // Fuzzy: try native select
1350
1333
  const res = await findFields.call(this, select, context)
1351
1334
  assertElementExists(res, select, 'Selectable field')
1352
- return proceedSelectOption.call(this, usingFirstElement(res), option)
1335
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1353
1336
  }
1354
1337
 
1355
1338
  /**
@@ -1358,7 +1341,7 @@ class WebDriver extends Helper {
1358
1341
  * {{> attachFile }}
1359
1342
  */
1360
1343
  async attachFile(locator, pathToFile, context = null) {
1361
- let file = path.join(global.codecept_dir, pathToFile)
1344
+ let file = path.join(store.codeceptDir, pathToFile)
1362
1345
  if (!fileExists(file)) {
1363
1346
  throw new Error(`File at ${file} can not be found on local system`)
1364
1347
  }
@@ -1367,7 +1350,7 @@ class WebDriver extends Helper {
1367
1350
  this.debug(`Uploading ${file}`)
1368
1351
 
1369
1352
  if (res.length) {
1370
- const el = usingFirstElement(res)
1353
+ const el = selectElement(res, locator, this)
1371
1354
  const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
1372
1355
  const type = await this.browser.execute(function (elem) { return elem.type }, el)
1373
1356
  if (tag === 'INPUT' && type === 'file') {
@@ -1385,7 +1368,7 @@ class WebDriver extends Helper {
1385
1368
 
1386
1369
  const targetRes = res.length ? res : await this._locate(locator)
1387
1370
  assertElementExists(targetRes, locator, 'Element')
1388
- const targetEl = usingFirstElement(targetRes)
1371
+ const targetEl = selectElement(targetRes, locator, this)
1389
1372
  const fileData = {
1390
1373
  base64Content: base64EncodeFile(file),
1391
1374
  fileName: path.basename(file),
@@ -1405,7 +1388,7 @@ class WebDriver extends Helper {
1405
1388
  const res = await findCheckable.call(this, field, locateFn)
1406
1389
 
1407
1390
  assertElementExists(res, field, 'Checkable')
1408
- const elem = usingFirstElement(res)
1391
+ const elem = selectElement(res, field, this)
1409
1392
  const elementId = getElementId(elem)
1410
1393
  highlightActiveElement.call(this, elem)
1411
1394
 
@@ -1426,7 +1409,7 @@ class WebDriver extends Helper {
1426
1409
  const res = await findCheckable.call(this, field, locateFn)
1427
1410
 
1428
1411
  assertElementExists(res, field, 'Checkable')
1429
- const elem = usingFirstElement(res)
1412
+ const elem = selectElement(res, field, this)
1430
1413
  const elementId = getElementId(elem)
1431
1414
  highlightActiveElement.call(this, elem)
1432
1415
 
@@ -1598,7 +1581,6 @@ class WebDriver extends Helper {
1598
1581
  /**
1599
1582
  * {{> see }}
1600
1583
  *
1601
- * {{ react }}
1602
1584
  */
1603
1585
  async see(text, context = null) {
1604
1586
  return proceedSee.call(this, 'assert', text, context)
@@ -1614,7 +1596,6 @@ class WebDriver extends Helper {
1614
1596
  /**
1615
1597
  * {{> dontSee }}
1616
1598
  *
1617
- * {{ react }}
1618
1599
  */
1619
1600
  async dontSee(text, context = null) {
1620
1601
  return proceedSee.call(this, 'negate', text, context)
@@ -1656,7 +1637,6 @@ class WebDriver extends Helper {
1656
1637
 
1657
1638
  /**
1658
1639
  * {{> seeElement }}
1659
- * {{ react }}
1660
1640
  *
1661
1641
  */
1662
1642
  async seeElement(locator, context = null) {
@@ -1673,7 +1653,6 @@ class WebDriver extends Helper {
1673
1653
 
1674
1654
  /**
1675
1655
  * {{> dontSeeElement }}
1676
- * {{ react }}
1677
1656
  */
1678
1657
  async dontSeeElement(locator, context = null) {
1679
1658
  const locateFn = prepareLocateFn.call(this, context)
@@ -1758,7 +1737,6 @@ class WebDriver extends Helper {
1758
1737
 
1759
1738
  /**
1760
1739
  * {{> seeNumberOfElements }}
1761
- * {{ react }}
1762
1740
  */
1763
1741
  async seeNumberOfElements(locator, num) {
1764
1742
  const res = await this._locate(locator)
@@ -1767,7 +1745,6 @@ class WebDriver extends Helper {
1767
1745
 
1768
1746
  /**
1769
1747
  * {{> seeNumberOfVisibleElements }}
1770
- * {{ react }}
1771
1748
  */
1772
1749
  async seeNumberOfVisibleElements(locator, num) {
1773
1750
  const res = await this.grabNumberOfVisibleElements(locator)
@@ -2243,6 +2220,7 @@ class WebDriver extends Helper {
2243
2220
  * {{> pressKeyWithKeyNormalization }}
2244
2221
  */
2245
2222
  async pressKey(key) {
2223
+ await checkFocusBeforePressKey(this, key)
2246
2224
  const modifiers = []
2247
2225
  if (Array.isArray(key)) {
2248
2226
  for (let k of key) {
@@ -2289,6 +2267,8 @@ class WebDriver extends Helper {
2289
2267
  * {{> type }}
2290
2268
  */
2291
2269
  async type(keys, delay = null) {
2270
+ await checkFocusBeforeType(this)
2271
+
2292
2272
  if (!Array.isArray(keys)) {
2293
2273
  keys = keys.toString()
2294
2274
  keys = keys.split('')
@@ -3301,6 +3281,19 @@ function assertElementExists(res, locator, prefix, suffix) {
3301
3281
  }
3302
3282
 
3303
3283
  function usingFirstElement(els) {
3284
+ const rawIndex = store.currentStep?.opts?.elementIndex
3285
+ if (rawIndex != null && els.length > 1) {
3286
+ let elementIndex = rawIndex
3287
+ if (elementIndex === 'first') elementIndex = 1
3288
+ if (elementIndex === 'last') elementIndex = -1
3289
+ if (Number.isInteger(elementIndex) && elementIndex !== 0) {
3290
+ const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
3291
+ if (idx >= 0 && idx < els.length) {
3292
+ debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
3293
+ return els[idx]
3294
+ }
3295
+ }
3296
+ }
3304
3297
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3305
3298
  return els[0]
3306
3299
  }
@@ -3473,7 +3466,7 @@ function isModifierKey(key) {
3473
3466
  }
3474
3467
 
3475
3468
  function highlightActiveElement(element) {
3476
- if (this.options.highlightElement && global.debugMode) {
3469
+ if (this.options.highlightElement && store.debugMode) {
3477
3470
  highlightElement(element, this.browser)
3478
3471
  }
3479
3472
  }
@@ -3484,9 +3477,6 @@ function prepareLocateFn(context) {
3484
3477
  l = new Locator(l, 'css')
3485
3478
  return this._locate(context, true).then(async res => {
3486
3479
  assertElementExists(res, context, 'Context element')
3487
- if (l.react) {
3488
- return res[0].react$$(l.react, l.props || undefined)
3489
- }
3490
3480
  return res[0].$$(l.simplify())
3491
3481
  })
3492
3482
  }
@@ -0,0 +1,8 @@
1
+ class NonFocusedType extends Error {
2
+ constructor(message) {
3
+ super(message)
4
+ this.name = 'NonFocusedType'
5
+ }
6
+ }
7
+
8
+ export default NonFocusedType
@@ -0,0 +1,45 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import minimatch from 'minimatch'
4
+ import store from '../../store.js'
5
+ import assert from 'assert'
6
+
7
+ function getDownloadDir() {
8
+ return path.join(store.outputDir, 'downloads')
9
+ }
10
+
11
+ function getNewFiles(downloadDir, sinceTimestamp) {
12
+ if (!fs.existsSync(downloadDir)) return []
13
+ return fs.readdirSync(downloadDir).filter(name => {
14
+ const stat = fs.statSync(path.join(downloadDir, name))
15
+ return stat.isFile() && stat.mtimeMs >= sinceTimestamp
16
+ })
17
+ }
18
+
19
+ function seeFileDownloaded(arg) {
20
+ const downloadDir = getDownloadDir()
21
+ const files = getNewFiles(downloadDir, this._downloadStartTimestamp)
22
+
23
+ if (arg === undefined || arg === null) {
24
+ assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`)
25
+ return
26
+ }
27
+ if (typeof arg === 'number') {
28
+ assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`)
29
+ return
30
+ }
31
+ const regexMatch = arg.match(/^\/(.+)\/$/)
32
+ if (regexMatch) {
33
+ const re = new RegExp(regexMatch[1])
34
+ assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`)
35
+ return
36
+ }
37
+ if (/[*?[\]]/.test(arg)) {
38
+ const matched = minimatch.match(files, arg)
39
+ assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`)
40
+ return
41
+ }
42
+ assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`)
43
+ }
44
+
45
+ export { seeFileDownloaded, getDownloadDir }
@@ -0,0 +1,10 @@
1
+ async function findByPlaywrightLocator(matcher, locator) {
2
+ const pwLocator = locator.locator || locator
3
+ if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
4
+ return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
5
+ }
6
+ const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
7
+ return matcher.locator(pwValue).all()
8
+ }
9
+
10
+ export { findByPlaywrightLocator }
@@ -0,0 +1,58 @@
1
+ import store from '../../store.js'
2
+ import output from '../../output.js'
3
+ import WebElement from '../../element/WebElement.js'
4
+ import MultipleElementsFound from '../errors/MultipleElementsFound.js'
5
+
6
+ function resolveElementIndex(value) {
7
+ if (value === 'first') return 1
8
+ if (value === 'last') return -1
9
+ return value
10
+ }
11
+
12
+ function isStrictStep(opts, helper) {
13
+ if (opts?.exact === true || opts?.strictMode === true) return true
14
+ if (opts?.exact === false || opts?.strictMode === false) return false
15
+ return helper.options.strict
16
+ }
17
+
18
+ function selectElement(els, locator, helper) {
19
+ const opts = store.currentStep?.opts
20
+ const rawIndex = opts?.elementIndex
21
+ const elementIndex = resolveElementIndex(rawIndex)
22
+
23
+ if (elementIndex != null) {
24
+ if (els.length === 1) return els[0]
25
+
26
+ if (!Number.isInteger(elementIndex) || elementIndex === 0) {
27
+ throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
28
+ }
29
+
30
+ let idx
31
+ if (elementIndex > 0) {
32
+ idx = elementIndex - 1
33
+ if (idx >= els.length) {
34
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
35
+ }
36
+ } else {
37
+ idx = els.length + elementIndex
38
+ if (idx < 0) {
39
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
40
+ }
41
+ }
42
+
43
+ output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
44
+ return els[idx]
45
+ }
46
+
47
+ if (isStrictStep(opts, helper)) {
48
+ if (els.length > 1) {
49
+ const webElements = Array.from(els).map(el => new WebElement(el, helper))
50
+ throw new MultipleElementsFound(locator, webElements)
51
+ }
52
+ }
53
+
54
+ if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
55
+ return els[0]
56
+ }
57
+
58
+ export { selectElement }
@@ -0,0 +1,43 @@
1
+ import store from '../../store.js'
2
+ import NonFocusedType from '../errors/NonFocusedType.js'
3
+
4
+ const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
5
+ const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
6
+
7
+ async function isNoElementFocused(helper) {
8
+ return helper.executeScript(() => {
9
+ const ae = document.activeElement
10
+ return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
11
+ })
12
+ }
13
+
14
+ export async function checkFocusBeforeType(helper) {
15
+ if (!helper.options.strict && !store.debugMode) return
16
+ if (!await isNoElementFocused(helper)) return
17
+
18
+ const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
19
+ if (helper.options.strict) throw new NonFocusedType(message)
20
+ helper.debugSection('Warning', message)
21
+ }
22
+
23
+ export async function checkFocusBeforePressKey(helper, originalKey) {
24
+ if (!helper.options.strict && !store.debugMode) return
25
+ if (!Array.isArray(originalKey)) return
26
+
27
+ let hasCtrlOrMeta = false
28
+ let actionKey = null
29
+ for (const k of originalKey) {
30
+ if (MODIFIER_PATTERN.test(k)) {
31
+ hasCtrlOrMeta = true
32
+ } else {
33
+ actionKey = k
34
+ }
35
+ }
36
+ if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
37
+
38
+ if (!await isNoElementFocused(helper)) return
39
+
40
+ const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
41
+ if (helper.options.strict) throw new NonFocusedType(message)
42
+ helper.debugSection('Warning', message)
43
+ }
@@ -0,0 +1,178 @@
1
+ import WebElement from '../../element/WebElement.js'
2
+
3
+ const MARKER = 'data-codeceptjs-rte-target'
4
+
5
+ const EDITOR = {
6
+ STANDARD: 'standard',
7
+ IFRAME: 'iframe',
8
+ CONTENTEDITABLE: 'contenteditable',
9
+ HIDDEN_TEXTAREA: 'hidden-textarea',
10
+ UNREACHABLE: 'unreachable',
11
+ }
12
+
13
+ function detectAndMark(el, opts) {
14
+ const marker = opts.marker
15
+ const kinds = opts.kinds
16
+ const CE = '[contenteditable="true"], [contenteditable=""]'
17
+
18
+ function mark(kind, target) {
19
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
20
+ if (target && target.nodeType === 1) target.setAttribute(marker, '1')
21
+ return kind
22
+ }
23
+
24
+ if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
25
+
26
+ const tag = el.tagName
27
+ if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
28
+ if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
29
+
30
+ const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
31
+ if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
32
+ const style = window.getComputedStyle(el)
33
+ if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
34
+ }
35
+
36
+ const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
37
+ if (canSearchDescendants) {
38
+ const iframe = el.querySelector('iframe')
39
+ if (iframe) return mark(kinds.IFRAME, iframe)
40
+ const ce = el.querySelector(CE)
41
+ if (ce) return mark(kinds.CONTENTEDITABLE, ce)
42
+ const textareas = [...el.querySelectorAll('textarea')]
43
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
44
+ const textarea = focusable || textareas[0]
45
+ if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
46
+ }
47
+
48
+ return mark(kinds.STANDARD, el)
49
+ }
50
+
51
+ function detectInsideFrame() {
52
+ const MARKER = 'data-codeceptjs-rte-target'
53
+ const CE = '[contenteditable="true"], [contenteditable=""]'
54
+ const CONTENTEDITABLE = 'contenteditable'
55
+ const HIDDEN_TEXTAREA = 'hidden-textarea'
56
+ const body = document.body
57
+ document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
58
+
59
+ if (body.isContentEditable) return CONTENTEDITABLE
60
+
61
+ const ce = body.querySelector(CE)
62
+ if (ce) {
63
+ ce.setAttribute(MARKER, '1')
64
+ return CONTENTEDITABLE
65
+ }
66
+
67
+ const textareas = [...body.querySelectorAll('textarea')]
68
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
69
+ const textarea = focusable || textareas[0]
70
+ if (textarea) {
71
+ textarea.setAttribute(MARKER, '1')
72
+ return HIDDEN_TEXTAREA
73
+ }
74
+
75
+ return CONTENTEDITABLE
76
+ }
77
+
78
+ async function evaluateInFrame(helper, body, fn) {
79
+ if (body.helperType === 'webdriver') {
80
+ return helper.executeScript(fn)
81
+ }
82
+ return body.element.evaluate(fn)
83
+ }
84
+
85
+ function focusMarkedInFrameScript() {
86
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
87
+ el.focus()
88
+ return document.activeElement === el
89
+ }
90
+
91
+ function selectAllInFrameScript() {
92
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
93
+ el.focus()
94
+ const range = document.createRange()
95
+ range.selectNodeContents(el)
96
+ const sel = window.getSelection()
97
+ sel.removeAllRanges()
98
+ sel.addRange(range)
99
+ return document.activeElement === el
100
+ }
101
+
102
+ function selectAllInEditable(el) {
103
+ const doc = el.ownerDocument
104
+ const win = doc.defaultView
105
+ el.focus()
106
+ const range = doc.createRange()
107
+ range.selectNodeContents(el)
108
+ const sel = win.getSelection()
109
+ sel.removeAllRanges()
110
+ sel.addRange(range)
111
+ }
112
+
113
+ function unmarkAll(marker) {
114
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
115
+ }
116
+
117
+ function isActive(el) {
118
+ return el.ownerDocument.activeElement === el
119
+ }
120
+
121
+ async function assertFocused(target) {
122
+ const focused = await target.evaluate(isActive)
123
+ if (!focused) {
124
+ throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
125
+ }
126
+ }
127
+
128
+ async function findMarked(helper) {
129
+ const root = helper.page || helper.browser
130
+ const raw = await root.$('[' + MARKER + ']')
131
+ return new WebElement(raw, helper)
132
+ }
133
+
134
+ async function clearMarker(helper) {
135
+ if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
136
+ return helper.executeScript(unmarkAll, MARKER)
137
+ }
138
+
139
+ export async function fillRichEditor(helper, el, value) {
140
+ const source = el instanceof WebElement ? el : new WebElement(el, helper)
141
+ const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
142
+ if (kind === EDITOR.STANDARD) return false
143
+ if (kind === EDITOR.UNREACHABLE) {
144
+ throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
145
+ }
146
+
147
+ const target = await findMarked(helper)
148
+ const delay = helper.options.pressKeyDelay
149
+
150
+ if (kind === EDITOR.IFRAME) {
151
+ await target.inIframe(async body => {
152
+ const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
153
+ if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
154
+ const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
155
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
156
+ await body.selectAllAndDelete()
157
+ await body.typeText(value, { delay })
158
+ } else {
159
+ const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
160
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
161
+ await body.typeText(value, { delay })
162
+ }
163
+ })
164
+ } else if (kind === EDITOR.HIDDEN_TEXTAREA) {
165
+ await target.focus()
166
+ await assertFocused(target)
167
+ await target.selectAllAndDelete()
168
+ await target.typeText(value, { delay })
169
+ } else if (kind === EDITOR.CONTENTEDITABLE) {
170
+ await target.click()
171
+ await target.evaluate(selectAllInEditable)
172
+ await assertFocused(target)
173
+ await target.typeText(value, { delay })
174
+ }
175
+
176
+ await clearMarker(helper)
177
+ return true
178
+ }