codeceptjs 4.0.2-beta.8 → 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 +91 -37
  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
@@ -8,6 +8,7 @@ import promiseRetry from 'promise-retry'
8
8
  import Locator from '../locator.js'
9
9
  import recorder from '../recorder.js'
10
10
  import store from '../store.js'
11
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
11
12
  import { includes as stringIncludes } from '../assert/include.js'
12
13
  import { urlEquals, equals } from '../assert/equal.js'
13
14
  import { empty } from '../assert/empty.js'
@@ -26,17 +27,25 @@ import {
26
27
  isModifierKey,
27
28
  requireWithFallback,
28
29
  normalizeSpacesInString,
30
+ normalizePath,
31
+ resolveUrl,
32
+ getMimeType,
33
+ base64EncodeFile,
29
34
  } from '../utils.js'
30
35
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
36
  import ElementNotFound from './errors/ElementNotFound.js'
37
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
32
38
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
33
39
  import Popup from './extras/Popup.js'
34
40
  import Console from './extras/Console.js'
35
41
  import { highlightElement } from './scripts/highlightElement.js'
36
42
  import { blurElement } from './scripts/blurElement.js'
43
+ import { dropFile } from './scripts/dropFile.js'
37
44
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
45
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
39
46
  import WebElement from '../element/WebElement.js'
47
+ import { selectElement } from './extras/elementSelection.js'
48
+ import { fillRichEditor } from './extras/richTextEditor.js'
40
49
 
41
50
  let puppeteer
42
51
 
@@ -266,6 +275,7 @@ class Puppeteer extends Helper {
266
275
  show: false,
267
276
  defaultPopupAction: 'accept',
268
277
  highlightElement: false,
278
+ strict: false,
269
279
  }
270
280
 
271
281
  return Object.assign(defaults, config)
@@ -743,7 +753,7 @@ class Puppeteer extends Helper {
743
753
  }
744
754
 
745
755
  if (this.options.trace) {
746
- const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
756
+ const fileName = `${`${store.outputDir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
747
757
  const dir = path.dirname(fileName)
748
758
  if (!fileExists(dir)) fs.mkdirSync(dir)
749
759
  await this.page.tracing.start({ screenshots: true, path: fileName })
@@ -811,12 +821,28 @@ class Puppeteer extends Helper {
811
821
 
812
822
  /**
813
823
  * {{> moveCursorTo }}
814
- * {{ react }}
815
824
  */
816
825
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
817
- const el = await this._locateElement(locator)
818
- if (!el) {
819
- throw new ElementNotFound(locator, 'Element to move cursor to')
826
+ let context = null
827
+ if (typeof offsetX !== 'number') {
828
+ context = offsetX
829
+ offsetX = 0
830
+ }
831
+
832
+ let el
833
+ if (context) {
834
+ const contextEls = await findElements.call(this, this.page, context)
835
+ assertElementExists(contextEls, context, 'Context element')
836
+ const els = await findElements.call(this, contextEls[0], locator)
837
+ if (!els || els.length === 0) {
838
+ throw new ElementNotFound(locator, 'Element to move cursor to')
839
+ }
840
+ el = els[0]
841
+ } else {
842
+ el = await this._locateElement(locator)
843
+ if (!el) {
844
+ throw new ElementNotFound(locator, 'Element to move cursor to')
845
+ }
820
846
  }
821
847
 
822
848
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -965,7 +991,6 @@ class Puppeteer extends Helper {
965
991
  * const elements = await this.helpers['Puppeteer']._locate({name: 'password'});
966
992
  * ```
967
993
  *
968
- * {{ react }}
969
994
  */
970
995
  async _locate(locator) {
971
996
  const context = await this.context
@@ -980,10 +1005,17 @@ class Puppeteer extends Helper {
980
1005
  * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
981
1006
  * ```
982
1007
  *
983
- * {{ react }}
984
1008
  */
985
1009
  async _locateElement(locator) {
986
1010
  const context = await this.context
1011
+ const elementIndex = store.currentStep?.opts?.elementIndex
1012
+ if (this.options.strict || elementIndex) {
1013
+ const elements = await findElements.call(this, context, locator)
1014
+ if (elements.length === 0) {
1015
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1016
+ }
1017
+ return selectElement(elements, locator, this)
1018
+ }
987
1019
  return findElement.call(this, context, locator)
988
1020
  }
989
1021
 
@@ -1001,7 +1033,7 @@ class Puppeteer extends Helper {
1001
1033
  if (!els || els.length === 0) {
1002
1034
  throw new ElementNotFound(locator, 'Checkbox or radio')
1003
1035
  }
1004
- return els[0]
1036
+ return selectElement(els, locator, this)
1005
1037
  }
1006
1038
 
1007
1039
  /**
@@ -1156,10 +1188,17 @@ class Puppeteer extends Helper {
1156
1188
 
1157
1189
  /**
1158
1190
  * {{> seeElement }}
1159
- * {{ react }}
1160
1191
  */
1161
- async seeElement(locator) {
1162
- let els = await this._locate(locator)
1192
+ async seeElement(locator, context = null) {
1193
+ let els
1194
+ if (context) {
1195
+ const contextPage = await this.context
1196
+ const contextEls = await findElements.call(this, contextPage, context)
1197
+ assertElementExists(contextEls, context, 'Context element')
1198
+ els = await findElements.call(this, contextEls[0], locator)
1199
+ } else {
1200
+ els = await this._locate(locator)
1201
+ }
1163
1202
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1164
1203
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1165
1204
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1172,10 +1211,17 @@ class Puppeteer extends Helper {
1172
1211
 
1173
1212
  /**
1174
1213
  * {{> dontSeeElement }}
1175
- * {{ react }}
1176
1214
  */
1177
- async dontSeeElement(locator) {
1178
- let els = await this._locate(locator)
1215
+ async dontSeeElement(locator, context = null) {
1216
+ let els
1217
+ if (context) {
1218
+ const contextPage = await this.context
1219
+ const contextEls = await findElements.call(this, contextPage, context)
1220
+ assertElementExists(contextEls, context, 'Context element')
1221
+ els = await findElements.call(this, contextEls[0], locator)
1222
+ } else {
1223
+ els = await this._locate(locator)
1224
+ }
1179
1225
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1180
1226
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1181
1227
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1213,7 +1259,6 @@ class Puppeteer extends Helper {
1213
1259
  /**
1214
1260
  * {{> click }}
1215
1261
  *
1216
- * {{ react }}
1217
1262
  */
1218
1263
  async click(locator = '//body', context = null) {
1219
1264
  return proceedClick.call(this, locator, context)
@@ -1222,7 +1267,6 @@ class Puppeteer extends Helper {
1222
1267
  /**
1223
1268
  * {{> forceClick }}
1224
1269
  *
1225
- * {{ react }}
1226
1270
  */
1227
1271
  async forceClick(locator, context = null) {
1228
1272
  let matcher = await this.context
@@ -1252,7 +1296,6 @@ class Puppeteer extends Helper {
1252
1296
  /**
1253
1297
  * {{> clickLink }}
1254
1298
  *
1255
- * {{ react }}
1256
1299
  */
1257
1300
  async clickLink(locator, context = null) {
1258
1301
  return proceedClick.call(this, locator, context, { waitForNavigation: true })
@@ -1276,7 +1319,7 @@ class Puppeteer extends Helper {
1276
1319
  * @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
1277
1320
  */
1278
1321
  async handleDownloads(downloadPath = 'downloads') {
1279
- downloadPath = path.join(global.output_dir, downloadPath)
1322
+ downloadPath = path.join(store.outputDir, downloadPath)
1280
1323
  if (!fs.existsSync(downloadPath)) {
1281
1324
  fs.mkdirSync(downloadPath, '0777')
1282
1325
  }
@@ -1338,7 +1381,7 @@ class Puppeteer extends Helper {
1338
1381
  },
1339
1382
  })
1340
1383
 
1341
- const outputFile = path.join(`${global.output_dir}/${fileName}`)
1384
+ const outputFile = path.join(`${store.outputDir}/${fileName}`)
1342
1385
 
1343
1386
  try {
1344
1387
  await new Promise((resolve, reject) => {
@@ -1360,7 +1403,6 @@ class Puppeteer extends Helper {
1360
1403
  /**
1361
1404
  * {{> doubleClick }}
1362
1405
  *
1363
- * {{ react }}
1364
1406
  */
1365
1407
  async doubleClick(locator, context = null) {
1366
1408
  return proceedClick.call(this, locator, context, { clickCount: 2 })
@@ -1369,7 +1411,6 @@ class Puppeteer extends Helper {
1369
1411
  /**
1370
1412
  * {{> rightClick }}
1371
1413
  *
1372
- * {{ react }}
1373
1414
  */
1374
1415
  async rightClick(locator, context = null) {
1375
1416
  return proceedClick.call(this, locator, context, { button: 'right' })
@@ -1498,6 +1539,7 @@ class Puppeteer extends Helper {
1498
1539
  * {{> pressKeyWithKeyNormalization }}
1499
1540
  */
1500
1541
  async pressKey(key) {
1542
+ await checkFocusBeforePressKey(this, key)
1501
1543
  const modifiers = []
1502
1544
  if (Array.isArray(key)) {
1503
1545
  for (let k of key) {
@@ -1526,6 +1568,8 @@ class Puppeteer extends Helper {
1526
1568
  * {{> type }}
1527
1569
  */
1528
1570
  async type(keys, delay = null) {
1571
+ await checkFocusBeforeType(this)
1572
+
1529
1573
  if (!Array.isArray(keys)) {
1530
1574
  keys = keys.toString()
1531
1575
  keys = keys.split('')
@@ -1539,12 +1583,20 @@ class Puppeteer extends Helper {
1539
1583
 
1540
1584
  /**
1541
1585
  * {{> fillField }}
1542
- * {{ react }}
1543
1586
  */
1544
- async fillField(field, value) {
1545
- const els = await findVisibleFields.call(this, field)
1587
+ async fillField(field, value, context = null) {
1588
+ let els = await findVisibleFields.call(this, field, context)
1589
+ if (!els.length) {
1590
+ els = await findFields.call(this, field, context)
1591
+ }
1546
1592
  assertElementExists(els, field, 'Field')
1547
- const el = els[0]
1593
+ const el = selectElement(els, field, this)
1594
+
1595
+ if (await fillRichEditor(this, el, value)) {
1596
+ highlightActiveElement.call(this, el, await this._getContext())
1597
+ return this._waitForAction()
1598
+ }
1599
+
1548
1600
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1549
1601
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1550
1602
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1562,38 +1614,38 @@ class Puppeteer extends Helper {
1562
1614
  /**
1563
1615
  * {{> clearField }}
1564
1616
  */
1565
- async clearField(field) {
1566
- return this.fillField(field, '')
1617
+ async clearField(field, context = null) {
1618
+ return this.fillField(field, '', context)
1567
1619
  }
1568
1620
 
1569
1621
  /**
1570
1622
  * {{> appendField }}
1571
1623
  *
1572
- * {{ react }}
1573
1624
  */
1574
- async appendField(field, value) {
1575
- const els = await findVisibleFields.call(this, field)
1625
+ async appendField(field, value, context = null) {
1626
+ const els = await findVisibleFields.call(this, field, context)
1576
1627
  assertElementExists(els, field, 'Field')
1577
- highlightActiveElement.call(this, els[0], await this._getContext())
1578
- await els[0].press('End')
1579
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
1628
+ const el = selectElement(els, field, this)
1629
+ highlightActiveElement.call(this, el, await this._getContext())
1630
+ await el.press('End')
1631
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1580
1632
  return this._waitForAction()
1581
1633
  }
1582
1634
 
1583
1635
  /**
1584
1636
  * {{> seeInField }}
1585
1637
  */
1586
- async seeInField(field, value) {
1638
+ async seeInField(field, value, context = null) {
1587
1639
  const _value = typeof value === 'boolean' ? value : value.toString()
1588
- return proceedSeeInField.call(this, 'assert', field, _value)
1640
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
1589
1641
  }
1590
1642
 
1591
1643
  /**
1592
1644
  * {{> dontSeeInField }}
1593
1645
  */
1594
- async dontSeeInField(field, value) {
1646
+ async dontSeeInField(field, value, context = null) {
1595
1647
  const _value = typeof value === 'boolean' ? value : value.toString()
1596
- return proceedSeeInField.call(this, 'negate', field, _value)
1648
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
1597
1649
  }
1598
1650
 
1599
1651
  /**
@@ -1601,54 +1653,75 @@ class Puppeteer extends Helper {
1601
1653
  *
1602
1654
  * {{> attachFile }}
1603
1655
  */
1604
- async attachFile(locator, pathToFile) {
1605
- const file = path.join(global.codecept_dir, pathToFile)
1656
+ async attachFile(locator, pathToFile, context = null) {
1657
+ const file = path.join(store.codeceptDir, pathToFile)
1606
1658
 
1607
1659
  if (!fileExists(file)) {
1608
1660
  throw new Error(`File at ${file} can not be found on local system`)
1609
1661
  }
1610
- const els = await findFields.call(this, locator)
1611
- assertElementExists(els, locator, 'Field')
1612
- await els[0].uploadFile(file)
1662
+ const els = await findFields.call(this, locator, context)
1663
+ if (els.length) {
1664
+ const el = selectElement(els, locator, this)
1665
+ const tag = await el.evaluate(el => el.tagName)
1666
+ const type = await el.evaluate(el => el.type)
1667
+ if (tag === 'INPUT' && type === 'file') {
1668
+ await el.uploadFile(file)
1669
+ return this._waitForAction()
1670
+ }
1671
+ }
1672
+
1673
+ const targetEls = els.length ? els : await this._locate(locator)
1674
+ assertElementExists(targetEls, locator, 'Element')
1675
+ const el = selectElement(targetEls, locator, this)
1676
+ const fileData = {
1677
+ base64Content: base64EncodeFile(file),
1678
+ fileName: path.basename(file),
1679
+ mimeType: getMimeType(path.basename(file)),
1680
+ }
1681
+ await el.evaluate(dropFile, fileData)
1613
1682
  return this._waitForAction()
1614
1683
  }
1615
1684
 
1616
1685
  /**
1617
1686
  * {{> selectOption }}
1618
1687
  */
1619
- async selectOption(select, option) {
1620
- const els = await findVisibleFields.call(this, select)
1621
- assertElementExists(els, select, 'Selectable field')
1622
- const el = els[0]
1623
- if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
1624
- throw new Error('Element is not <select>')
1625
- }
1626
- highlightActiveElement.call(this, els[0], await this._getContext())
1627
- if (!Array.isArray(option)) option = [option]
1628
-
1629
- for (const key in option) {
1630
- const opt = xpathLocator.literal(option[key])
1631
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
1632
- if (optEl.length) {
1633
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1634
- continue
1635
- }
1636
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
1637
- if (optEl.length) {
1638
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1639
- }
1688
+ async selectOption(select, option, context = null) {
1689
+ const pageContext = await this._getContext()
1690
+ const matchedLocator = new Locator(select)
1691
+
1692
+ let contextEl
1693
+ if (context) {
1694
+ const contextEls = await findElements.call(this, pageContext, context)
1695
+ assertElementExists(contextEls, context, 'Context element')
1696
+ contextEl = contextEls[0]
1640
1697
  }
1641
- await this._evaluateHandeInContext(element => {
1642
- element.dispatchEvent(new Event('input', { bubbles: true }))
1643
- element.dispatchEvent(new Event('change', { bubbles: true }))
1644
- }, el)
1645
1698
 
1646
- return this._waitForAction()
1699
+ // Strict locator
1700
+ if (!matchedLocator.isFuzzy()) {
1701
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1702
+ const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1703
+ assertElementExists(els, select, 'Selectable element')
1704
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1705
+ }
1706
+
1707
+ // Fuzzy: try combobox
1708
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1709
+ const comboboxSearchCtx = contextEl || pageContext
1710
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1711
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1712
+
1713
+ // Fuzzy: try listbox
1714
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1715
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1716
+
1717
+ // Fuzzy: try native select
1718
+ const visibleEls = await findVisibleFields.call(this, select, context)
1719
+ assertElementExists(visibleEls, select, 'Selectable field')
1720
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1647
1721
  }
1648
1722
 
1649
1723
  /**
1650
1724
  * {{> grabNumberOfVisibleElements }}
1651
- * {{ react }}
1652
1725
  */
1653
1726
  async grabNumberOfVisibleElements(locator) {
1654
1727
  let els = await this._locate(locator)
@@ -1687,10 +1760,29 @@ class Puppeteer extends Helper {
1687
1760
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
1688
1761
  }
1689
1762
 
1763
+ /**
1764
+ * {{> seeCurrentPathEquals }}
1765
+ */
1766
+ async seeCurrentPathEquals(path) {
1767
+ const currentUrl = await this._getPageUrl()
1768
+ const baseUrl = this.options.url || 'http://localhost'
1769
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1770
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1771
+ }
1772
+
1773
+ /**
1774
+ * {{> dontSeeCurrentPathEquals }}
1775
+ */
1776
+ async dontSeeCurrentPathEquals(path) {
1777
+ const currentUrl = await this._getPageUrl()
1778
+ const baseUrl = this.options.url || 'http://localhost'
1779
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1780
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1781
+ }
1782
+
1690
1783
  /**
1691
1784
  * {{> see }}
1692
1785
  *
1693
- * {{ react }}
1694
1786
  */
1695
1787
  async see(text, context = null) {
1696
1788
  return proceedSee.call(this, 'assert', text, context)
@@ -1706,7 +1798,6 @@ class Puppeteer extends Helper {
1706
1798
  /**
1707
1799
  * {{> dontSee }}
1708
1800
  *
1709
- * {{ react }}
1710
1801
  */
1711
1802
  async dontSee(text, context = null) {
1712
1803
  return proceedSee.call(this, 'negate', text, context)
@@ -1760,7 +1851,6 @@ class Puppeteer extends Helper {
1760
1851
  /**
1761
1852
  * {{> seeNumberOfElements }}
1762
1853
  *
1763
- * {{ react }}
1764
1854
  */
1765
1855
  async seeNumberOfElements(locator, num) {
1766
1856
  const elements = await this._locate(locator)
@@ -1770,7 +1860,6 @@ class Puppeteer extends Helper {
1770
1860
  /**
1771
1861
  * {{> seeNumberOfVisibleElements }}
1772
1862
  *
1773
- * {{ react }}
1774
1863
  */
1775
1864
  async seeNumberOfVisibleElements(locator, num) {
1776
1865
  const res = await this.grabNumberOfVisibleElements(locator)
@@ -1897,7 +1986,6 @@ class Puppeteer extends Helper {
1897
1986
 
1898
1987
  /**
1899
1988
  * {{> grabTextFromAll }}
1900
- * {{ react }}
1901
1989
  */
1902
1990
  async grabTextFromAll(locator) {
1903
1991
  const els = await this._locate(locator)
@@ -1910,7 +1998,6 @@ class Puppeteer extends Helper {
1910
1998
 
1911
1999
  /**
1912
2000
  * {{> grabTextFrom }}
1913
- * {{ react }}
1914
2001
  */
1915
2002
  async grabTextFrom(locator) {
1916
2003
  const texts = await this.grabTextFromAll(locator)
@@ -1971,7 +2058,6 @@ class Puppeteer extends Helper {
1971
2058
 
1972
2059
  /**
1973
2060
  * {{> grabCssPropertyFromAll }}
1974
- * {{ react }}
1975
2061
  */
1976
2062
  async grabCssPropertyFromAll(locator, cssProperty) {
1977
2063
  const els = await this._locate(locator)
@@ -1983,7 +2069,6 @@ class Puppeteer extends Helper {
1983
2069
 
1984
2070
  /**
1985
2071
  * {{> grabCssPropertyFrom }}
1986
- * {{ react }}
1987
2072
  */
1988
2073
  async grabCssPropertyFrom(locator, cssProperty) {
1989
2074
  const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
@@ -1998,7 +2083,6 @@ class Puppeteer extends Helper {
1998
2083
 
1999
2084
  /**
2000
2085
  * {{> seeCssPropertiesOnElements }}
2001
- * {{ react }}
2002
2086
  */
2003
2087
  async seeCssPropertiesOnElements(locator, cssProperties) {
2004
2088
  const res = await this._locate(locator)
@@ -2033,7 +2117,6 @@ class Puppeteer extends Helper {
2033
2117
 
2034
2118
  /**
2035
2119
  * {{> seeAttributesOnElements }}
2036
- * {{ react }}
2037
2120
  */
2038
2121
  async seeAttributesOnElements(locator, attributes) {
2039
2122
  const elements = await this._locate(locator)
@@ -2071,7 +2154,6 @@ class Puppeteer extends Helper {
2071
2154
 
2072
2155
  /**
2073
2156
  * {{> dragSlider }}
2074
- * {{ react }}
2075
2157
  */
2076
2158
  async dragSlider(locator, offsetX = 0) {
2077
2159
  const src = await this._locate(locator)
@@ -2093,7 +2175,6 @@ class Puppeteer extends Helper {
2093
2175
 
2094
2176
  /**
2095
2177
  * {{> grabAttributeFromAll }}
2096
- * {{ react }}
2097
2178
  */
2098
2179
  async grabAttributeFromAll(locator, attr) {
2099
2180
  const els = await this._locate(locator)
@@ -2107,7 +2188,6 @@ class Puppeteer extends Helper {
2107
2188
 
2108
2189
  /**
2109
2190
  * {{> grabAttributeFrom }}
2110
- * {{ react }}
2111
2191
  */
2112
2192
  async grabAttributeFrom(locator, attr) {
2113
2193
  const attrs = await this.grabAttributeFromAll(locator, attr)
@@ -2270,7 +2350,6 @@ class Puppeteer extends Helper {
2270
2350
 
2271
2351
  /**
2272
2352
  * {{> waitNumberOfVisibleElements }}
2273
- * {{ react }}
2274
2353
  */
2275
2354
  async waitNumberOfVisibleElements(locator, num, sec) {
2276
2355
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
@@ -2318,7 +2397,6 @@ class Puppeteer extends Helper {
2318
2397
 
2319
2398
  /**
2320
2399
  * {{> waitForElement }}
2321
- * {{ react }}
2322
2400
  */
2323
2401
  async waitForElement(locator, sec) {
2324
2402
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
@@ -2339,7 +2417,6 @@ class Puppeteer extends Helper {
2339
2417
  /**
2340
2418
  * {{> waitForVisible }}
2341
2419
  *
2342
- * {{ react }}
2343
2420
  */
2344
2421
  async waitForVisible(locator, sec) {
2345
2422
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
@@ -2424,6 +2501,7 @@ class Puppeteer extends Helper {
2424
2501
  */
2425
2502
  async waitInUrl(urlPart, sec = null) {
2426
2503
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2504
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2427
2505
 
2428
2506
  return this.page
2429
2507
  .waitForFunction(
@@ -2432,12 +2510,12 @@ class Puppeteer extends Helper {
2432
2510
  return currUrl.indexOf(urlPart) > -1
2433
2511
  },
2434
2512
  { timeout: waitTimeout },
2435
- urlPart,
2513
+ expectedUrl,
2436
2514
  )
2437
2515
  .catch(async e => {
2438
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2516
+ const currUrl = await this._getPageUrl()
2439
2517
  if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2440
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2518
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2441
2519
  } else {
2442
2520
  throw e
2443
2521
  }
@@ -2449,25 +2527,50 @@ class Puppeteer extends Helper {
2449
2527
  */
2450
2528
  async waitUrlEquals(urlPart, sec = null) {
2451
2529
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2530
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2452
2531
 
2453
- const baseUrl = this.options.url
2454
- if (urlPart.indexOf('http') < 0) {
2455
- urlPart = baseUrl + urlPart
2456
- }
2532
+ return this.page
2533
+ .waitForFunction(
2534
+ url => {
2535
+ const currUrl = decodeURIComponent(window.location.href)
2536
+ return currUrl === url
2537
+ },
2538
+ { timeout: waitTimeout },
2539
+ expectedUrl,
2540
+ )
2541
+ .catch(async e => {
2542
+ const currUrl = await this._getPageUrl()
2543
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2544
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2545
+ } else {
2546
+ throw e
2547
+ }
2548
+ })
2549
+ }
2550
+
2551
+ /**
2552
+ * {{> waitCurrentPathEquals }}
2553
+ */
2554
+ async waitCurrentPathEquals(path, sec = null) {
2555
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2556
+ const normalizedPath = normalizePath(path)
2457
2557
 
2458
2558
  return this.page
2459
2559
  .waitForFunction(
2460
- urlPart => {
2461
- const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2462
- return currUrl.indexOf(urlPart) > -1
2560
+ expectedPath => {
2561
+ const actualPath = window.location.pathname
2562
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
2563
+ return normalizePath(actualPath) === expectedPath
2463
2564
  },
2464
2565
  { timeout: waitTimeout },
2465
- urlPart,
2566
+ normalizedPath,
2466
2567
  )
2467
2568
  .catch(async e => {
2468
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2569
+ const currUrl = await this._getPageUrl()
2570
+ const baseUrl = this.options.url || 'http://localhost'
2571
+ const actualPath = new URL(currUrl, baseUrl).pathname
2469
2572
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2470
- throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
2573
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2471
2574
  } else {
2472
2575
  throw e
2473
2576
  }
@@ -2890,15 +2993,18 @@ export default Puppeteer
2890
2993
  * @returns {Promise<Array>} Array of ElementHandle objects
2891
2994
  */
2892
2995
  async function findElements(matcher, locator) {
2893
- // Check if locator is a Locator object with react type, or a raw object with react property
2894
- const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
2895
- if (isReactLocator) return findReactElements.call(this, locator)
2896
-
2897
2996
  locator = new Locator(locator, 'css')
2898
-
2997
+
2899
2998
  // Check if locator is a role locator and call findByRole
2900
2999
  if (locator.isRole()) return findByRole.call(this, matcher, locator)
2901
3000
 
3001
+ // Handle shadow DOM locators with >>> deep descendant combinator
3002
+ // { shadow: ['my-app', 'recipe-hello', 'button'] } => 'my-app >>> recipe-hello >>> button'
3003
+ if (locator.isShadow()) {
3004
+ const shadowSelector = locator.value.join(' >>> ')
3005
+ return matcher.$$(shadowSelector)
3006
+ }
3007
+
2902
3008
  // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
2903
3009
  if (!locator.isXPath()) return matcher.$$(locator.simplify())
2904
3010
 
@@ -2953,7 +3059,6 @@ async function findElements(matcher, locator) {
2953
3059
  * @returns {Promise<Object>} Single ElementHandle object
2954
3060
  */
2955
3061
  async function findElement(matcher, locator) {
2956
- if (locator.react) return findReactElements.call(this, locator)
2957
3062
  locator = new Locator(locator, 'css')
2958
3063
 
2959
3064
  // Check if locator is a role locator and call findByRole
@@ -2962,6 +3067,13 @@ async function findElement(matcher, locator) {
2962
3067
  return elements[0]
2963
3068
  }
2964
3069
 
3070
+ // Handle shadow DOM locators with >>> deep descendant combinator
3071
+ if (locator.isShadow()) {
3072
+ const shadowSelector = locator.value.join(' >>> ')
3073
+ const elements = await matcher.$$(shadowSelector)
3074
+ return elements[0]
3075
+ }
3076
+
2965
3077
  // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2966
3078
  if (!locator.isXPath()) {
2967
3079
  const elements = await matcher.$$(locator.simplify())
@@ -2990,10 +3102,11 @@ async function proceedClick(locator, context = null, options = {}) {
2990
3102
  } else {
2991
3103
  assertElementExists(els, locator, 'Clickable element')
2992
3104
  }
3105
+ const el = selectElement(els, locator, this)
2993
3106
 
2994
- highlightActiveElement.call(this, els[0], await this._getContext())
3107
+ highlightActiveElement.call(this, el, await this._getContext())
2995
3108
 
2996
- await els[0].click(options)
3109
+ await el.click(options)
2997
3110
  const promises = []
2998
3111
  if (options.waitForNavigation) {
2999
3112
  promises.push(this.waitForNavigation())
@@ -3124,43 +3237,57 @@ async function proceedIsChecked(assertType, option) {
3124
3237
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3125
3238
  }
3126
3239
 
3127
- async function findVisibleFields(locator) {
3128
- const els = await findFields.call(this, locator)
3240
+ async function findVisibleFields(locator, context = null) {
3241
+ const els = await findFields.call(this, locator, context)
3129
3242
  const visible = await Promise.all(els.map(el => el.boundingBox()))
3130
3243
  return els.filter((el, index) => visible[index])
3131
3244
  }
3132
3245
 
3133
- async function findFields(locator) {
3246
+ async function findFields(locator, context = null) {
3247
+ let contextEl
3248
+ if (context) {
3249
+ const contextPage = await this.context
3250
+ const contextEls = await findElements.call(this, contextPage, context)
3251
+ assertElementExists(contextEls, context, 'Context element')
3252
+ contextEl = contextEls[0]
3253
+ }
3254
+
3255
+ const locateFn = contextEl
3256
+ ? loc => findElements.call(this, contextEl, loc)
3257
+ : loc => this._locate(loc)
3258
+
3134
3259
  const matchedLocator = new Locator(locator)
3135
3260
  if (!matchedLocator.isFuzzy()) {
3136
- return this._locate(matchedLocator)
3261
+ return locateFn(matchedLocator)
3137
3262
  }
3138
3263
  const literal = xpathLocator.literal(matchedLocator.value)
3139
3264
 
3140
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
3265
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
3141
3266
  if (els.length) {
3142
3267
  return els
3143
3268
  }
3144
3269
 
3145
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
3270
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
3146
3271
  if (els.length) {
3147
3272
  return els
3148
3273
  }
3149
- els = await this._locate({ xpath: Locator.field.byName(literal) })
3274
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
3150
3275
  if (els.length) {
3151
3276
  return els
3152
3277
  }
3153
3278
 
3154
3279
  // Try ARIA selector for accessible name
3155
- try {
3156
- const page = await this.context
3157
- els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3158
- if (els.length) return els
3159
- } catch (err) {
3160
- // ARIA selector not supported or failed
3280
+ if (!contextEl) {
3281
+ try {
3282
+ const page = await this.context
3283
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3284
+ if (els.length) return els
3285
+ } catch (err) {
3286
+ // ARIA selector not supported or failed
3287
+ }
3161
3288
  }
3162
3289
 
3163
- return this._locate({ css: matchedLocator.value })
3290
+ return locateFn({ css: matchedLocator.value })
3164
3291
  }
3165
3292
 
3166
3293
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -3189,8 +3316,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3189
3316
  await this._waitForAction()
3190
3317
  }
3191
3318
 
3192
- async function proceedSeeInField(assertType, field, value) {
3193
- const els = await findVisibleFields.call(this, field)
3319
+ async function proceedSeeInField(assertType, field, value, context) {
3320
+ const els = await findVisibleFields.call(this, field, context)
3194
3321
  assertElementExists(els, field, 'Field')
3195
3322
  const el = els[0]
3196
3323
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
@@ -3303,6 +3430,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3303
3430
  }
3304
3431
  }
3305
3432
 
3433
+ function assertOnlyOneElement(elements, locator, helper) {
3434
+ if (elements.length > 1) {
3435
+ const webElements = elements.map(el => new WebElement(el, helper))
3436
+ throw new MultipleElementsFound(locator, webElements)
3437
+ }
3438
+ }
3439
+
3306
3440
  function $XPath(element, selector) {
3307
3441
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3308
3442
  const res = []
@@ -3410,7 +3544,7 @@ function getNormalizedKey(key) {
3410
3544
  }
3411
3545
 
3412
3546
  function highlightActiveElement(element, context) {
3413
- if (this.options.highlightElement && global.debugMode) {
3547
+ if (this.options.highlightElement && store.debugMode) {
3414
3548
  highlightElement(element, context)
3415
3549
  }
3416
3550
  }
@@ -3423,75 +3557,6 @@ function _waitForElement(locator, options) {
3423
3557
  }
3424
3558
  }
3425
3559
 
3426
- async function findReactElements(locator) {
3427
- // Handle both Locator objects and raw locator objects
3428
- const resolved = locator.locator ? locator.locator : toLocatorConfig(locator, 'react')
3429
- this.debug(`Finding React elements: ${JSON.stringify(resolved)}`)
3430
-
3431
- // Use createRequire to access require.resolve in ESM
3432
- const { createRequire } = await import('module')
3433
- const require = createRequire(import.meta.url)
3434
- const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
3435
- await this.page.evaluate(resqScript.toString())
3436
-
3437
- await this.page.evaluate(() => window.resq.waitToLoadReact())
3438
- const arrayHandle = await this.page.evaluateHandle(
3439
- obj => {
3440
- const { selector, props, state } = obj
3441
- let elements = window.resq.resq$$(selector)
3442
- if (Object.keys(props).length) {
3443
- elements = elements.byProps(props)
3444
- }
3445
- if (Object.keys(state).length) {
3446
- elements = elements.byState(state)
3447
- }
3448
-
3449
- if (!elements.length) {
3450
- return []
3451
- }
3452
-
3453
- // resq returns an array of HTMLElements if the React component is a fragment
3454
- // this avoids having nested arrays of nodes which the driver does not understand
3455
- // [[div, div], [div, div]] => [div, div, div, div]
3456
- let nodes = []
3457
-
3458
- elements.forEach(element => {
3459
- let { node, isFragment } = element
3460
-
3461
- if (!node) {
3462
- isFragment = true
3463
- node = element.children
3464
- }
3465
-
3466
- if (isFragment) {
3467
- nodes = nodes.concat(node)
3468
- } else {
3469
- nodes.push(node)
3470
- }
3471
- })
3472
-
3473
- return [...nodes]
3474
- },
3475
- {
3476
- selector: resolved.react,
3477
- props: resolved.props || {},
3478
- state: resolved.state || {},
3479
- },
3480
- )
3481
-
3482
- const properties = await arrayHandle.getProperties()
3483
- const result = []
3484
- for (const property of properties.values()) {
3485
- const elementHandle = property.asElement()
3486
- if (elementHandle) {
3487
- result.push(elementHandle)
3488
- }
3489
- }
3490
-
3491
- await arrayHandle.dispose()
3492
- return result
3493
- }
3494
-
3495
3560
  async function findByRole(matcher, locator) {
3496
3561
  const resolved = toLocatorConfig(locator, 'role')
3497
3562
  const roleSelector = buildRoleSelector(resolved)
@@ -3541,3 +3606,81 @@ function createRoleTextMatcher(expected, exactMatch) {
3541
3606
  return value => typeof value === 'string' && value.includes(target)
3542
3607
  }
3543
3608
 
3609
+ async function proceedSelect(context, el, option) {
3610
+ const role = await el.evaluate(e => e.getAttribute('role'))
3611
+ const options = Array.isArray(option) ? option : [option]
3612
+
3613
+ if (role === 'combobox') {
3614
+ this.debugSection('SelectOption', 'Expanding combobox')
3615
+ highlightActiveElement.call(this, el, context)
3616
+ const ariaOwns = await el.evaluate(e => e.getAttribute('aria-owns'))
3617
+ const ariaControls = await el.evaluate(e => e.getAttribute('aria-controls'))
3618
+ await el.click()
3619
+ await this._waitForAction()
3620
+
3621
+ const listboxId = ariaOwns || ariaControls
3622
+ let listbox = null
3623
+ if (listboxId) {
3624
+ const listboxEls = await context.$$( `#${listboxId}`)
3625
+ if (listboxEls.length) listbox = listboxEls[0]
3626
+ }
3627
+ if (!listbox) {
3628
+ const listboxEls = await findByRole.call(this, context, { role: 'listbox' })
3629
+ if (listboxEls?.length) listbox = listboxEls[0]
3630
+ }
3631
+
3632
+ if (listbox) {
3633
+ for (const opt of options) {
3634
+ const optEls = await findByRole.call(this, listbox, { role: 'option', name: opt })
3635
+ if (optEls?.length) {
3636
+ const optEl = optEls[0]
3637
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3638
+ highlightActiveElement.call(this, optEl, context)
3639
+ await optEl.click()
3640
+ }
3641
+ }
3642
+ }
3643
+ return this._waitForAction()
3644
+ }
3645
+
3646
+ if (role === 'listbox') {
3647
+ for (const opt of options) {
3648
+ const optEls = await findByRole.call(this, el, { role: 'option', name: opt })
3649
+ if (optEls?.length) {
3650
+ const optEl = optEls[0]
3651
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3652
+ highlightActiveElement.call(this, optEl, context)
3653
+ await optEl.click()
3654
+ }
3655
+ }
3656
+ return this._waitForAction()
3657
+ }
3658
+
3659
+ // Native <select> element
3660
+ const tagName = await el.evaluate(e => e.tagName)
3661
+ if (tagName !== 'SELECT') {
3662
+ throw new Error('Element is not <select>')
3663
+ }
3664
+ highlightActiveElement.call(this, el, context)
3665
+ const optionArray = Array.isArray(option) ? option : [option]
3666
+
3667
+ for (const key in optionArray) {
3668
+ const opt = xpathLocator.literal(optionArray[key])
3669
+ let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
3670
+ if (optEl.length) {
3671
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
3672
+ continue
3673
+ }
3674
+ optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
3675
+ if (optEl.length) {
3676
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
3677
+ }
3678
+ }
3679
+ await this._evaluateHandeInContext(element => {
3680
+ element.dispatchEvent(new Event('input', { bubbles: true }))
3681
+ element.dispatchEvent(new Event('change', { bubbles: true }))
3682
+ }, el)
3683
+
3684
+ return this._waitForAction()
3685
+ }
3686
+