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
@@ -1,5 +1,6 @@
1
1
  let webdriverio
2
2
 
3
+ import fs from 'fs'
3
4
  import assert from 'assert'
4
5
  import path from 'path'
5
6
  import crypto from 'crypto'
@@ -9,21 +10,39 @@ import promiseRetry from 'promise-retry'
9
10
  import { includes as stringIncludes } from '../assert/include.js'
10
11
  import { urlEquals, equals } from '../assert/equal.js'
11
12
  import store from '../store.js'
13
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
12
14
  import output from '../output.js'
13
15
  const { debug } = output
14
16
  import { empty } from '../assert/empty.js'
15
17
  import { truth } from '../assert/truth.js'
16
- import { xpathLocator, fileExists, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, modifierKeys } from '../utils.js'
18
+ import {
19
+ xpathLocator,
20
+ fileExists,
21
+ decodeUrl,
22
+ chunkArray,
23
+ convertCssPropertiesToCamelCase,
24
+ screenshotOutputFolder,
25
+ getNormalizedKeyAttributeValue,
26
+ modifierKeys,
27
+ normalizePath,
28
+ resolveUrl,
29
+ getMimeType,
30
+ base64EncodeFile,
31
+ } from '../utils.js'
17
32
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
18
33
  import ElementNotFound from './errors/ElementNotFound.js'
34
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
19
35
  import ConnectionRefused from './errors/ConnectionRefused.js'
20
36
  import Locator from '../locator.js'
21
37
  import { highlightElement } from './scripts/highlightElement.js'
22
38
  import { focusElement } from './scripts/focusElement.js'
23
39
  import { blurElement } from './scripts/blurElement.js'
24
40
  import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
41
+ import { dropFile } from './scripts/dropFile.js'
25
42
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
26
43
  import WebElement from '../element/WebElement.js'
44
+ import { selectElement } from './extras/elementSelection.js'
45
+ import { fillRichEditor } from './extras/richTextEditor.js'
27
46
 
28
47
  const SHADOW = 'shadow'
29
48
  const webRoot = 'body'
@@ -78,11 +97,7 @@ const config = {}
78
97
  * WebDriver helper which wraps [webdriverio](http://webdriver.io/) library to
79
98
  * manipulate browser using Selenium WebDriver or PhantomJS.
80
99
  *
81
- * 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.
82
- *
83
- * 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/).
84
- * 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
85
- * `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.
86
101
  *
87
102
  * For those who require custom driver options, fear not; WebDriver Helper allows you to pass in driver options through custom WebDriver configuration.
88
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.
@@ -489,6 +504,7 @@ class WebDriver extends Helper {
489
504
  keepBrowserState: false,
490
505
  deprecationWarnings: false,
491
506
  highlightElement: false,
507
+ strict: false,
492
508
  }
493
509
 
494
510
  // override defaults with config
@@ -888,13 +904,6 @@ class WebDriver extends Helper {
888
904
  return els
889
905
  }
890
906
 
891
- // special locator type for React
892
- if (locator.react) {
893
- const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined)
894
- this.debugSection('Elements', `Found ${els.length} react components`)
895
- return els
896
- }
897
-
898
907
  // special locator type for ARIA roles
899
908
  if (locator.role) {
900
909
  return this._locateByRole(locator)
@@ -1064,7 +1073,6 @@ class WebDriver extends Helper {
1064
1073
  /**
1065
1074
  * {{> click }}
1066
1075
  *
1067
- * {{ react }}
1068
1076
  */
1069
1077
  async click(locator, context = null) {
1070
1078
  const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
@@ -1076,7 +1084,7 @@ class WebDriver extends Helper {
1076
1084
  } else {
1077
1085
  assertElementExists(res, locator, 'Clickable element')
1078
1086
  }
1079
- const elem = usingFirstElement(res)
1087
+ const elem = selectElement(res, locator, this)
1080
1088
  highlightActiveElement.call(this, elem)
1081
1089
  return this.browser[clickMethod](getElementId(elem))
1082
1090
  }
@@ -1084,7 +1092,6 @@ class WebDriver extends Helper {
1084
1092
  /**
1085
1093
  * {{> forceClick }}
1086
1094
  *
1087
- * {{ react }}
1088
1095
  */
1089
1096
  async forceClick(locator, context = null) {
1090
1097
  const locateFn = prepareLocateFn.call(this, context)
@@ -1095,7 +1102,7 @@ class WebDriver extends Helper {
1095
1102
  } else {
1096
1103
  assertElementExists(res, locator, 'Clickable element')
1097
1104
  }
1098
- const elem = usingFirstElement(res)
1105
+ const elem = selectElement(res, locator, this)
1099
1106
  highlightActiveElement.call(this, elem)
1100
1107
 
1101
1108
  return this.executeScript(el => {
@@ -1111,7 +1118,6 @@ class WebDriver extends Helper {
1111
1118
  /**
1112
1119
  * {{> doubleClick }}
1113
1120
  *
1114
- * {{ react }}
1115
1121
  */
1116
1122
  async doubleClick(locator, context = null) {
1117
1123
  const locateFn = prepareLocateFn.call(this, context)
@@ -1123,7 +1129,7 @@ class WebDriver extends Helper {
1123
1129
  assertElementExists(res, locator, 'Clickable element')
1124
1130
  }
1125
1131
 
1126
- const elem = usingFirstElement(res)
1132
+ const elem = selectElement(res, locator, this)
1127
1133
  highlightActiveElement.call(this, elem)
1128
1134
  return elem.doubleClick()
1129
1135
  }
@@ -1131,7 +1137,6 @@ class WebDriver extends Helper {
1131
1137
  /**
1132
1138
  * {{> rightClick }}
1133
1139
  *
1134
- * {{ react }}
1135
1140
  */
1136
1141
  async rightClick(locator, context) {
1137
1142
  const locateFn = prepareLocateFn.call(this, context)
@@ -1143,7 +1148,7 @@ class WebDriver extends Helper {
1143
1148
  assertElementExists(res, locator, 'Clickable element')
1144
1149
  }
1145
1150
 
1146
- const el = usingFirstElement(res)
1151
+ const el = selectElement(res, locator, this)
1147
1152
 
1148
1153
  await el.moveTo()
1149
1154
 
@@ -1226,7 +1231,6 @@ class WebDriver extends Helper {
1226
1231
  /**
1227
1232
  * {{> forceRightClick }}
1228
1233
  *
1229
- * {{ react }}
1230
1234
  */
1231
1235
  async forceRightClick(locator, context = null) {
1232
1236
  const locateFn = prepareLocateFn.call(this, context)
@@ -1251,15 +1255,19 @@ class WebDriver extends Helper {
1251
1255
 
1252
1256
  /**
1253
1257
  * {{> fillField }}
1254
- * {{ react }}
1255
1258
  * {{ custom }}
1256
1259
  *
1257
1260
  */
1258
- async fillField(field, value) {
1259
- const res = await findFields.call(this, field)
1261
+ async fillField(field, value, context = null) {
1262
+ const res = await findFields.call(this, field, context)
1260
1263
  assertElementExists(res, field, 'Field')
1261
- const elem = usingFirstElement(res)
1264
+ const elem = selectElement(res, field, this)
1262
1265
  highlightActiveElement.call(this, elem)
1266
+
1267
+ if (await fillRichEditor(this, elem, value)) {
1268
+ return
1269
+ }
1270
+
1263
1271
  try {
1264
1272
  await elem.clearValue()
1265
1273
  } catch (err) {
@@ -1276,12 +1284,11 @@ class WebDriver extends Helper {
1276
1284
 
1277
1285
  /**
1278
1286
  * {{> appendField }}
1279
- * {{ react }}
1280
1287
  */
1281
- async appendField(field, value) {
1282
- const res = await findFields.call(this, field)
1288
+ async appendField(field, value, context = null) {
1289
+ const res = await findFields.call(this, field, context)
1283
1290
  assertElementExists(res, field, 'Field')
1284
- const elem = usingFirstElement(res)
1291
+ const elem = selectElement(res, field, this)
1285
1292
  highlightActiveElement.call(this, elem)
1286
1293
  return elem.addValue(value.toString())
1287
1294
  }
@@ -1290,10 +1297,10 @@ class WebDriver extends Helper {
1290
1297
  * {{> clearField }}
1291
1298
  *
1292
1299
  */
1293
- async clearField(field) {
1294
- const res = await findFields.call(this, field)
1300
+ async clearField(field, context = null) {
1301
+ const res = await findFields.call(this, field, context)
1295
1302
  assertElementExists(res, field, 'Field')
1296
- const elem = usingFirstElement(res)
1303
+ const elem = selectElement(res, field, this)
1297
1304
  highlightActiveElement.call(this, elem)
1298
1305
  return elem.clearValue(getElementId(elem))
1299
1306
  }
@@ -1301,34 +1308,31 @@ class WebDriver extends Helper {
1301
1308
  /**
1302
1309
  * {{> selectOption }}
1303
1310
  */
1304
- async selectOption(select, option) {
1305
- const res = await findFields.call(this, select)
1306
- assertElementExists(res, select, 'Selectable field')
1307
- const elem = usingFirstElement(res)
1308
- highlightActiveElement.call(this, elem)
1311
+ async selectOption(select, option, context = null) {
1312
+ const locateFn = prepareLocateFn.call(this, context)
1313
+ const matchedLocator = new Locator(select)
1309
1314
 
1310
- if (!Array.isArray(option)) {
1311
- option = [option]
1315
+ // Strict locator
1316
+ if (!matchedLocator.isFuzzy()) {
1317
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1318
+ const els = await locateFn(select)
1319
+ assertElementExists(els, select, 'Selectable element')
1320
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1312
1321
  }
1313
1322
 
1314
- // select options by visible text
1315
- let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
1323
+ // Fuzzy: try combobox
1324
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1325
+ let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1326
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1316
1327
 
1317
- const clickOptionFn = async el => {
1318
- if (el[0]) el = el[0]
1319
- const elementId = getElementId(el)
1320
- if (elementId) return this.browser.elementClick(elementId)
1321
- }
1328
+ // Fuzzy: try listbox
1329
+ els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1330
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1322
1331
 
1323
- if (Array.isArray(els) && els.length) {
1324
- return forEachAsync(els, clickOptionFn)
1325
- }
1326
- // select options by value
1327
- els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
1328
- if (els.length === 0) {
1329
- throw new ElementNotFound(select, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
1330
- }
1331
- return forEachAsync(els, clickOptionFn)
1332
+ // Fuzzy: try native select
1333
+ const res = await findFields.call(this, select, context)
1334
+ assertElementExists(res, select, 'Selectable field')
1335
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1332
1336
  }
1333
1337
 
1334
1338
  /**
@@ -1336,28 +1340,41 @@ class WebDriver extends Helper {
1336
1340
  *
1337
1341
  * {{> attachFile }}
1338
1342
  */
1339
- async attachFile(locator, pathToFile) {
1340
- let file = path.join(global.codecept_dir, pathToFile)
1343
+ async attachFile(locator, pathToFile, context = null) {
1344
+ let file = path.join(store.codeceptDir, pathToFile)
1341
1345
  if (!fileExists(file)) {
1342
1346
  throw new Error(`File at ${file} can not be found on local system`)
1343
1347
  }
1344
1348
 
1345
- const res = await findFields.call(this, locator)
1349
+ const res = await findFields.call(this, locator, context)
1346
1350
  this.debug(`Uploading ${file}`)
1347
- assertElementExists(res, locator, 'File field')
1348
- const el = usingFirstElement(res)
1349
1351
 
1350
- // Remote Upload (when running Selenium Server)
1351
- if (this.options.remoteFileUpload) {
1352
- try {
1353
- this.debugSection('File', 'Uploading file to remote server')
1354
- file = await this.browser.uploadFile(file)
1355
- } catch (err) {
1356
- throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
1352
+ if (res.length) {
1353
+ const el = selectElement(res, locator, this)
1354
+ const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
1355
+ const type = await this.browser.execute(function (elem) { return elem.type }, el)
1356
+ if (tag === 'INPUT' && type === 'file') {
1357
+ if (this.options.remoteFileUpload) {
1358
+ try {
1359
+ this.debugSection('File', 'Uploading file to remote server')
1360
+ file = await this.browser.uploadFile(file)
1361
+ } catch (err) {
1362
+ throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
1363
+ }
1364
+ }
1365
+ return el.addValue(file)
1357
1366
  }
1358
1367
  }
1359
1368
 
1360
- return el.addValue(file)
1369
+ const targetRes = res.length ? res : await this._locate(locator)
1370
+ assertElementExists(targetRes, locator, 'Element')
1371
+ const targetEl = selectElement(targetRes, locator, this)
1372
+ const fileData = {
1373
+ base64Content: base64EncodeFile(file),
1374
+ fileName: path.basename(file),
1375
+ mimeType: getMimeType(path.basename(file)),
1376
+ }
1377
+ return this.browser.execute(dropFile, targetEl, fileData)
1361
1378
  }
1362
1379
 
1363
1380
  /**
@@ -1371,7 +1388,7 @@ class WebDriver extends Helper {
1371
1388
  const res = await findCheckable.call(this, field, locateFn)
1372
1389
 
1373
1390
  assertElementExists(res, field, 'Checkable')
1374
- const elem = usingFirstElement(res)
1391
+ const elem = selectElement(res, field, this)
1375
1392
  const elementId = getElementId(elem)
1376
1393
  highlightActiveElement.call(this, elem)
1377
1394
 
@@ -1392,7 +1409,7 @@ class WebDriver extends Helper {
1392
1409
  const res = await findCheckable.call(this, field, locateFn)
1393
1410
 
1394
1411
  assertElementExists(res, field, 'Checkable')
1395
- const elem = usingFirstElement(res)
1412
+ const elem = selectElement(res, field, this)
1396
1413
  const elementId = getElementId(elem)
1397
1414
  highlightActiveElement.call(this, elem)
1398
1415
 
@@ -1564,7 +1581,6 @@ class WebDriver extends Helper {
1564
1581
  /**
1565
1582
  * {{> see }}
1566
1583
  *
1567
- * {{ react }}
1568
1584
  */
1569
1585
  async see(text, context = null) {
1570
1586
  return proceedSee.call(this, 'assert', text, context)
@@ -1580,7 +1596,6 @@ class WebDriver extends Helper {
1580
1596
  /**
1581
1597
  * {{> dontSee }}
1582
1598
  *
1583
- * {{ react }}
1584
1599
  */
1585
1600
  async dontSee(text, context = null) {
1586
1601
  return proceedSee.call(this, 'negate', text, context)
@@ -1590,18 +1605,18 @@ class WebDriver extends Helper {
1590
1605
  * {{> seeInField }}
1591
1606
  *
1592
1607
  */
1593
- async seeInField(field, value) {
1608
+ async seeInField(field, value, context = null) {
1594
1609
  const _value = typeof value === 'boolean' ? value : value.toString()
1595
- return proceedSeeField.call(this, 'assert', field, _value)
1610
+ return proceedSeeField.call(this, 'assert', field, _value, context)
1596
1611
  }
1597
1612
 
1598
1613
  /**
1599
1614
  * {{> dontSeeInField }}
1600
1615
  *
1601
1616
  */
1602
- async dontSeeInField(field, value) {
1617
+ async dontSeeInField(field, value, context = null) {
1603
1618
  const _value = typeof value === 'boolean' ? value : value.toString()
1604
- return proceedSeeField.call(this, 'negate', field, _value)
1619
+ return proceedSeeField.call(this, 'negate', field, _value, context)
1605
1620
  }
1606
1621
 
1607
1622
  /**
@@ -1622,11 +1637,11 @@ class WebDriver extends Helper {
1622
1637
 
1623
1638
  /**
1624
1639
  * {{> seeElement }}
1625
- * {{ react }}
1626
1640
  *
1627
1641
  */
1628
- async seeElement(locator) {
1629
- const res = await this._locate(locator, true)
1642
+ async seeElement(locator, context = null) {
1643
+ const locateFn = prepareLocateFn.call(this, context)
1644
+ const res = context ? await locateFn(locator) : await this._locate(locator, true)
1630
1645
  assertElementExists(res, locator)
1631
1646
  const selected = await forEachAsync(res, async el => el.isDisplayed())
1632
1647
  try {
@@ -1638,10 +1653,10 @@ class WebDriver extends Helper {
1638
1653
 
1639
1654
  /**
1640
1655
  * {{> dontSeeElement }}
1641
- * {{ react }}
1642
1656
  */
1643
- async dontSeeElement(locator) {
1644
- const res = await this._locate(locator, false)
1657
+ async dontSeeElement(locator, context = null) {
1658
+ const locateFn = prepareLocateFn.call(this, context)
1659
+ const res = context ? await locateFn(locator) : await this._locate(locator, false)
1645
1660
  if (!res || res.length === 0) {
1646
1661
  return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
1647
1662
  }
@@ -1722,7 +1737,6 @@ class WebDriver extends Helper {
1722
1737
 
1723
1738
  /**
1724
1739
  * {{> seeNumberOfElements }}
1725
- * {{ react }}
1726
1740
  */
1727
1741
  async seeNumberOfElements(locator, num) {
1728
1742
  const res = await this._locate(locator)
@@ -1731,7 +1745,6 @@ class WebDriver extends Helper {
1731
1745
 
1732
1746
  /**
1733
1747
  * {{> seeNumberOfVisibleElements }}
1734
- * {{ react }}
1735
1748
  */
1736
1749
  async seeNumberOfVisibleElements(locator, num) {
1737
1750
  const res = await this.grabNumberOfVisibleElements(locator)
@@ -1848,6 +1861,26 @@ class WebDriver extends Helper {
1848
1861
  return urlEquals(this.options.url).negate(url, decodeUrl(res))
1849
1862
  }
1850
1863
 
1864
+ /**
1865
+ * {{> seeCurrentPathEquals }}
1866
+ */
1867
+ async seeCurrentPathEquals(path) {
1868
+ const currentUrl = await this.browser.getUrl()
1869
+ const baseUrl = this.options.url || 'http://localhost'
1870
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1871
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1872
+ }
1873
+
1874
+ /**
1875
+ * {{> dontSeeCurrentPathEquals }}
1876
+ */
1877
+ async dontSeeCurrentPathEquals(path) {
1878
+ const currentUrl = await this.browser.getUrl()
1879
+ const baseUrl = this.options.url || 'http://localhost'
1880
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1881
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1882
+ }
1883
+
1851
1884
  /**
1852
1885
  * Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
1853
1886
  *
@@ -1920,8 +1953,22 @@ class WebDriver extends Helper {
1920
1953
  * {{> moveCursorTo }}
1921
1954
  */
1922
1955
  async moveCursorTo(locator, xOffset, yOffset) {
1923
- const res = await this._locate(withStrictLocator(locator), true)
1924
- assertElementExists(res, locator)
1956
+ let context = null
1957
+ if (typeof xOffset !== 'number' && xOffset !== undefined) {
1958
+ context = xOffset
1959
+ xOffset = undefined
1960
+ }
1961
+
1962
+ let res
1963
+ if (context) {
1964
+ const contextRes = await this._locate(withStrictLocator(context), true)
1965
+ assertElementExists(contextRes, context, 'Context element')
1966
+ res = await contextRes[0].$$(withStrictLocator(locator))
1967
+ assertElementExists(res, locator)
1968
+ } else {
1969
+ res = await this._locate(withStrictLocator(locator), true)
1970
+ assertElementExists(res, locator)
1971
+ }
1925
1972
  const elem = usingFirstElement(res)
1926
1973
  try {
1927
1974
  await elem.moveTo({ xOffset, yOffset })
@@ -2173,6 +2220,7 @@ class WebDriver extends Helper {
2173
2220
  * {{> pressKeyWithKeyNormalization }}
2174
2221
  */
2175
2222
  async pressKey(key) {
2223
+ await checkFocusBeforePressKey(this, key)
2176
2224
  const modifiers = []
2177
2225
  if (Array.isArray(key)) {
2178
2226
  for (let k of key) {
@@ -2219,6 +2267,8 @@ class WebDriver extends Helper {
2219
2267
  * {{> type }}
2220
2268
  */
2221
2269
  async type(keys, delay = null) {
2270
+ await checkFocusBeforeType(this)
2271
+
2222
2272
  if (!Array.isArray(keys)) {
2223
2273
  keys = keys.toString()
2224
2274
  keys = keys.split('')
@@ -2471,6 +2521,7 @@ class WebDriver extends Helper {
2471
2521
  async waitInUrl(urlPart, sec = null) {
2472
2522
  const client = this.browser
2473
2523
  const aSec = sec || this.options.waitForTimeoutInSeconds
2524
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2474
2525
  let currUrl = ''
2475
2526
 
2476
2527
  return client
@@ -2478,7 +2529,7 @@ class WebDriver extends Helper {
2478
2529
  function () {
2479
2530
  return this.getUrl().then(res => {
2480
2531
  currUrl = decodeUrl(res)
2481
- return currUrl.indexOf(urlPart) > -1
2532
+ return currUrl.indexOf(expectedUrl) > -1
2482
2533
  })
2483
2534
  },
2484
2535
  { timeout: aSec * 1000 },
@@ -2486,7 +2537,7 @@ class WebDriver extends Helper {
2486
2537
  .catch(e => {
2487
2538
  e = wrapError(e)
2488
2539
  if (e.message.indexOf('timeout')) {
2489
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2540
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2490
2541
  }
2491
2542
  throw e
2492
2543
  })
@@ -2497,22 +2548,47 @@ class WebDriver extends Helper {
2497
2548
  */
2498
2549
  async waitUrlEquals(urlPart, sec = null) {
2499
2550
  const aSec = sec || this.options.waitForTimeoutInSeconds
2500
- const baseUrl = this.options.url
2501
- if (urlPart.indexOf('http') < 0) {
2502
- urlPart = baseUrl + urlPart
2503
- }
2551
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2504
2552
  let currUrl = ''
2505
2553
  return this.browser
2506
2554
  .waitUntil(function () {
2507
2555
  return this.getUrl().then(res => {
2508
2556
  currUrl = decodeUrl(res)
2509
- return currUrl === urlPart
2557
+ return currUrl === expectedUrl
2510
2558
  })
2511
2559
  }, aSec * 1000)
2512
2560
  .catch(e => {
2513
2561
  e = wrapError(e)
2514
2562
  if (e.message.indexOf('timeout')) {
2515
- throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
2563
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2564
+ }
2565
+ throw e
2566
+ })
2567
+ }
2568
+
2569
+ /**
2570
+ * {{> waitCurrentPathEquals }}
2571
+ */
2572
+ async waitCurrentPathEquals(path, sec = null) {
2573
+ const aSec = sec || this.options.waitForTimeoutInSeconds
2574
+ const normalizedPath = normalizePath(path)
2575
+ const baseUrl = this.options.url || 'http://localhost'
2576
+ let actualPath = ''
2577
+
2578
+ return this.browser
2579
+ .waitUntil(
2580
+ async () => {
2581
+ const currUrl = await this.browser.getUrl()
2582
+ const url = new URL(currUrl, baseUrl)
2583
+ actualPath = url.pathname
2584
+ return normalizePath(actualPath) === normalizedPath
2585
+ },
2586
+ { timeout: aSec * 1000 },
2587
+ )
2588
+ .catch(e => {
2589
+ e = wrapError(e)
2590
+ if (e.message.indexOf('timeout')) {
2591
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2516
2592
  }
2517
2593
  throw e
2518
2594
  })
@@ -2994,32 +3070,33 @@ async function findClickable(locator, locateFn) {
2994
3070
  return await locateFn(locator.value) // by css or xpath
2995
3071
  }
2996
3072
 
2997
- async function findFields(locator) {
3073
+ async function findFields(locator, context = null) {
3074
+ const locateFn = prepareLocateFn.call(this, context)
2998
3075
  locator = new Locator(locator)
2999
3076
 
3000
3077
  if (this._isCustomLocator(locator)) {
3001
- return this._locate(locator)
3078
+ return locateFn(locator)
3002
3079
  }
3003
3080
 
3004
- if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true)
3005
- if (locator.isRole()) return this._locate(locator, true)
3006
- if (!locator.isFuzzy()) return this._locate(locator, true)
3081
+ if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator)
3082
+ if (locator.isRole()) return locateFn(locator)
3083
+ if (!locator.isFuzzy()) return locateFn(locator)
3007
3084
 
3008
3085
  const literal = xpathLocator.literal(locator.value)
3009
- let els = await this._locate(Locator.field.labelEquals(literal))
3086
+ let els = await locateFn(Locator.field.labelEquals(literal))
3010
3087
  if (els.length) return els
3011
3088
 
3012
- els = await this._locate(Locator.field.labelContains(literal))
3089
+ els = await locateFn(Locator.field.labelContains(literal))
3013
3090
  if (els.length) return els
3014
3091
 
3015
- els = await this._locate(Locator.field.byName(literal))
3092
+ els = await locateFn(Locator.field.byName(literal))
3016
3093
  if (els.length) return els
3017
3094
 
3018
- return await this._locate(locator.value) // by css or xpath
3095
+ return await locateFn(locator.value) // by css or xpath
3019
3096
  }
3020
3097
 
3021
- async function proceedSeeField(assertType, field, value) {
3022
- const res = await findFields.call(this, field)
3098
+ async function proceedSeeField(assertType, field, value, context) {
3099
+ const res = await findFields.call(this, field, context)
3023
3100
  assertElementExists(res, field, 'Field')
3024
3101
  const elem = usingFirstElement(res)
3025
3102
  const elemId = getElementId(elem)
@@ -3128,7 +3205,23 @@ async function getElementTextAttributes(element) {
3128
3205
  const ariaLabel = await this.browser.getElementAttribute(elementId, 'aria-label').catch(() => '')
3129
3206
  const placeholder = await this.browser.getElementAttribute(elementId, 'placeholder').catch(() => '')
3130
3207
  const innerText = await this.browser.getElementText(elementId).catch(() => '')
3131
- return [ariaLabel, placeholder, innerText]
3208
+
3209
+ // Handle aria-labelledby
3210
+ const labelledBy = await this.browser.getElementAttribute(elementId, 'aria-labelledby').catch(() => '')
3211
+ let labelText = ''
3212
+ if (labelledBy) {
3213
+ try {
3214
+ const labelId = labelledBy.split(' ')[0]
3215
+ const labelEls = await this.browser.$$(`#${labelId}`)
3216
+ if (labelEls?.length) {
3217
+ labelText = await this.browser.getElementText(getElementId(labelEls[0])).catch(() => '')
3218
+ }
3219
+ } catch (e) {
3220
+ // Ignore errors when resolving aria-labelledby
3221
+ }
3222
+ }
3223
+
3224
+ return [ariaLabel, placeholder, innerText, labelText]
3132
3225
  }
3133
3226
 
3134
3227
  async function isElementChecked(browser, elementId) {
@@ -3188,10 +3281,30 @@ function assertElementExists(res, locator, prefix, suffix) {
3188
3281
  }
3189
3282
 
3190
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
+ }
3191
3297
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3192
3298
  return els[0]
3193
3299
  }
3194
3300
 
3301
+ function assertOnlyOneElement(elements, locator, helper) {
3302
+ if (elements.length > 1) {
3303
+ const webElements = Array.from(elements).map(el => new WebElement(el, helper))
3304
+ throw new MultipleElementsFound(locator, webElements)
3305
+ }
3306
+ }
3307
+
3195
3308
  function getElementId(el) {
3196
3309
  // W3C WebDriver web element identifier
3197
3310
  // https://w3c.github.io/webdriver/#dfn-web-element-identifier
@@ -3353,7 +3466,7 @@ function isModifierKey(key) {
3353
3466
  }
3354
3467
 
3355
3468
  function highlightActiveElement(element) {
3356
- if (this.options.highlightElement && global.debugMode) {
3469
+ if (this.options.highlightElement && store.debugMode) {
3357
3470
  highlightElement(element, this.browser)
3358
3471
  }
3359
3472
  }
@@ -3364,9 +3477,6 @@ function prepareLocateFn(context) {
3364
3477
  l = new Locator(l, 'css')
3365
3478
  return this._locate(context, true).then(async res => {
3366
3479
  assertElementExists(res, context, 'Context element')
3367
- if (l.react) {
3368
- return res[0].react$$(l.react, l.props || undefined)
3369
- }
3370
3480
  return res[0].$$(l.simplify())
3371
3481
  })
3372
3482
  }
@@ -3376,4 +3486,107 @@ function logEvents(event) {
3376
3486
  browserLogs.push(event.text) // add log message to the array
3377
3487
  }
3378
3488
 
3489
+ async function proceedSelectOption(elem, option) {
3490
+ const elementId = getElementId(elem)
3491
+ const role = await this.browser.getElementAttribute(elementId, 'role').catch(() => null)
3492
+ const options = Array.isArray(option) ? option : [option]
3493
+
3494
+ if (role === 'combobox') {
3495
+ this.debugSection('SelectOption', 'Expanding combobox')
3496
+ highlightActiveElement.call(this, elem)
3497
+ const ariaOwns = await this.browser.getElementAttribute(elementId, 'aria-owns').catch(() => null)
3498
+ const ariaControls = await this.browser.getElementAttribute(elementId, 'aria-controls').catch(() => null)
3499
+ const ariaLabelledBy = await this.browser.getElementAttribute(elementId, 'aria-labelledby').catch(() => null)
3500
+ await this.browser.elementClick(elementId)
3501
+
3502
+ const listboxId = ariaOwns || ariaControls
3503
+ let listbox = null
3504
+ if (listboxId) {
3505
+ const listboxEls = await this.browser.$$(`#${listboxId}`)
3506
+ if (listboxEls?.length) listbox = listboxEls[0]
3507
+ }
3508
+ if (!listbox && ariaLabelledBy) {
3509
+ // Find listbox with the same aria-labelledby
3510
+ const listboxEls = await this.browser.$$(`[role="listbox"][aria-labelledby="${ariaLabelledBy}"]`)
3511
+ if (listboxEls?.length) listbox = listboxEls[0]
3512
+ }
3513
+ if (!listbox) {
3514
+ // Fallback: find any listbox with the same label
3515
+ const allListboxes = await this.browser.$$(`[role="listbox"]`)
3516
+ for (const lb of allListboxes) {
3517
+ const lbLabelledBy = await this.browser.getElementAttribute(getElementId(lb), 'aria-labelledby').catch(() => '')
3518
+ if (lbLabelledBy === ariaLabelledBy) {
3519
+ listbox = lb
3520
+ break
3521
+ }
3522
+ }
3523
+ }
3524
+
3525
+ if (listbox) {
3526
+ const listboxElementId = getElementId(listbox)
3527
+ for (const opt of options) {
3528
+ const optEls = await this.browser.findElementsFromElement(listboxElementId, 'xpath', `.//*[@role="option"]`)
3529
+ if (optEls?.length) {
3530
+ for (const optEl of optEls) {
3531
+ const optElId = getElementId(optEl)
3532
+ const text = await this.browser.getElementText(optElId).catch(() => '')
3533
+ if (text && text.includes(opt)) {
3534
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3535
+ highlightActiveElement.call(this, optEl)
3536
+ await this.browser.elementClick(optElId)
3537
+ break
3538
+ }
3539
+ }
3540
+ }
3541
+ }
3542
+ }
3543
+ return
3544
+ }
3545
+
3546
+ if (role === 'listbox') {
3547
+ for (const opt of options) {
3548
+ const optEls = await this.browser.findElementsFromElement(elementId, 'xpath', `.//*[@role="option"]`)
3549
+ if (optEls?.length) {
3550
+ for (const optEl of optEls) {
3551
+ const optElId = getElementId(optEl)
3552
+ const text = await this.browser.getElementText(optElId).catch(() => '')
3553
+ if (text && text.includes(opt)) {
3554
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3555
+ highlightActiveElement.call(this, optEl)
3556
+ await this.browser.elementClick(optElId)
3557
+ break
3558
+ }
3559
+ }
3560
+ }
3561
+ }
3562
+ return
3563
+ }
3564
+
3565
+ // Native <select> element
3566
+ highlightActiveElement.call(this, elem)
3567
+
3568
+ if (!Array.isArray(option)) {
3569
+ option = [option]
3570
+ }
3571
+
3572
+ const clickOptionFn = async el => {
3573
+ if (el[0]) el = el[0]
3574
+ const elId = getElementId(el)
3575
+ if (elId) return this.browser.elementClick(elId)
3576
+ }
3577
+
3578
+ // select options by visible text
3579
+ let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
3580
+
3581
+ if (Array.isArray(els) && els.length) {
3582
+ return forEachAsync(els, clickOptionFn)
3583
+ }
3584
+ // select options by value
3585
+ els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
3586
+ if (els.length === 0) {
3587
+ throw new ElementNotFound(elem, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
3588
+ }
3589
+ return forEachAsync(els, clickOptionFn)
3590
+ }
3591
+
3379
3592
  export { WebDriver as default }