codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21

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 (296) hide show
  1. package/README.md +39 -27
  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 +537 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/assertions.md +415 -0
  11. package/docs/auth.md +318 -0
  12. package/docs/basics.md +424 -0
  13. package/docs/bdd.md +539 -0
  14. package/docs/best.md +240 -0
  15. package/docs/bootstrap.md +132 -0
  16. package/docs/commands.md +352 -0
  17. package/docs/community-helpers.md +63 -0
  18. package/docs/configuration.md +230 -0
  19. package/docs/continuous-integration.md +497 -0
  20. package/docs/custom-helpers.md +297 -0
  21. package/docs/data.md +448 -0
  22. package/docs/debugging.md +332 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/docker.md +136 -0
  25. package/docs/effects.md +179 -0
  26. package/docs/element-based-testing.md +295 -0
  27. package/docs/element-selection.md +125 -0
  28. package/docs/els.md +328 -0
  29. package/docs/environment-variables.md +131 -0
  30. package/docs/examples.md +161 -0
  31. package/docs/heal.md +213 -0
  32. package/docs/helpers/ApiDataFactory.md +267 -0
  33. package/docs/helpers/Appium.md +1405 -0
  34. package/docs/helpers/Detox.md +665 -0
  35. package/docs/helpers/ExpectHelper.md +275 -0
  36. package/docs/helpers/FileSystem.md +152 -0
  37. package/docs/helpers/GraphQL.md +152 -0
  38. package/docs/helpers/GraphQLDataFactory.md +226 -0
  39. package/docs/helpers/JSONResponse.md +255 -0
  40. package/docs/helpers/Mochawesome.md +8 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/MockServer.md +212 -0
  43. package/docs/helpers/Playwright.md +2969 -0
  44. package/docs/helpers/Polly.md +44 -0
  45. package/docs/helpers/Protractor.md +1769 -0
  46. package/docs/helpers/Puppeteer-firefox.md +86 -0
  47. package/docs/helpers/Puppeteer.md +2690 -0
  48. package/docs/helpers/REST.md +289 -0
  49. package/docs/helpers/SoftExpectHelper.md +352 -0
  50. package/docs/helpers/WebDriver.md +2682 -0
  51. package/docs/hooks.md +339 -0
  52. package/docs/index.md +111 -0
  53. package/docs/installation.md +83 -0
  54. package/docs/internal-api.md +265 -0
  55. package/docs/internal-test-server.md +89 -0
  56. package/docs/locators.md +355 -0
  57. package/docs/mcp.md +485 -0
  58. package/docs/migration-4.md +556 -0
  59. package/docs/mobile.md +338 -0
  60. package/docs/pageobjects.md +399 -0
  61. package/docs/parallel.md +585 -0
  62. package/docs/playwright.md +714 -0
  63. package/docs/plugins.md +866 -0
  64. package/docs/puppeteer.md +314 -0
  65. package/docs/quickstart.md +120 -0
  66. package/docs/react.md +70 -0
  67. package/docs/reports.md +483 -0
  68. package/docs/retry.md +274 -0
  69. package/docs/secrets.md +150 -0
  70. package/docs/sessions.md +80 -0
  71. package/docs/shadow.md +68 -0
  72. package/docs/test-structure.md +275 -0
  73. package/docs/timeouts.md +183 -0
  74. package/docs/translation.md +247 -0
  75. package/docs/tutorial.md +271 -0
  76. package/docs/typescript.md +374 -0
  77. package/docs/web-element.md +251 -0
  78. package/docs/webdriver.md +708 -0
  79. package/docs/within.md +55 -0
  80. package/lib/ai.js +3 -2
  81. package/lib/aria.js +260 -0
  82. package/lib/assertions.js +18 -0
  83. package/lib/codecept.js +27 -24
  84. package/lib/command/check.js +2 -1
  85. package/lib/command/dryRun.js +24 -5
  86. package/lib/command/generate.js +2 -0
  87. package/lib/command/gherkin/snippets.js +5 -4
  88. package/lib/command/init.js +248 -269
  89. package/lib/command/list.js +150 -10
  90. package/lib/command/query.js +218 -0
  91. package/lib/command/run-multiple.js +2 -0
  92. package/lib/command/run-workers.js +2 -14
  93. package/lib/command/run.js +3 -17
  94. package/lib/command/utils.js +14 -0
  95. package/lib/command/workers/runTests.js +10 -10
  96. package/lib/config.js +77 -4
  97. package/lib/container.js +114 -17
  98. package/lib/effects.js +17 -0
  99. package/lib/element/WebElement.js +246 -2
  100. package/lib/els.js +12 -6
  101. package/lib/globals.js +32 -19
  102. package/lib/heal.js +6 -3
  103. package/lib/helper/ApiDataFactory.js +2 -1
  104. package/lib/helper/Appium.js +8 -8
  105. package/lib/helper/FileSystem.js +3 -2
  106. package/lib/helper/GraphQLDataFactory.js +2 -1
  107. package/lib/helper/Playwright.js +233 -162
  108. package/lib/helper/Puppeteer.js +208 -76
  109. package/lib/helper/WebDriver.js +173 -68
  110. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  111. package/lib/helper/errors/NonFocusedType.js +8 -0
  112. package/lib/helper/extras/Download.js +45 -0
  113. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  114. package/lib/helper/extras/elementSelection.js +58 -0
  115. package/lib/helper/extras/focusCheck.js +43 -0
  116. package/lib/helper/extras/richTextEditor.js +178 -0
  117. package/lib/helper/scripts/dropFile.js +11 -0
  118. package/lib/history.js +3 -2
  119. package/lib/html.js +103 -16
  120. package/lib/index.js +9 -1
  121. package/lib/listener/config.js +6 -4
  122. package/lib/listener/emptyRun.js +2 -1
  123. package/lib/listener/globalRetry.js +32 -6
  124. package/lib/listener/helpers.js +4 -1
  125. package/lib/listener/mocha.js +2 -1
  126. package/lib/listener/pageobjects.js +43 -0
  127. package/lib/listener/result.js +3 -2
  128. package/lib/locator.js +126 -3
  129. package/lib/mocha/cli.js +14 -2
  130. package/lib/mocha/factory.js +7 -2
  131. package/lib/mocha/inject.js +1 -1
  132. package/lib/mocha/scenarioConfig.js +2 -1
  133. package/lib/mocha/ui.js +5 -6
  134. package/lib/parser.js +2 -2
  135. package/lib/pause.js +38 -4
  136. package/lib/plugin/aiTrace.js +456 -0
  137. package/lib/plugin/analyze.js +6 -5
  138. package/lib/plugin/auth.js +3 -3
  139. package/lib/plugin/browser.js +77 -0
  140. package/lib/plugin/expose.js +159 -0
  141. package/lib/plugin/heal.js +47 -3
  142. package/lib/plugin/pageInfo.js +54 -52
  143. package/lib/plugin/pause.js +131 -0
  144. package/lib/plugin/pauseOnFail.js +10 -34
  145. package/lib/plugin/retryFailedStep.js +32 -22
  146. package/lib/plugin/screencast.js +289 -0
  147. package/lib/plugin/screenshot.js +563 -0
  148. package/lib/plugin/screenshotOnFail.js +8 -171
  149. package/lib/rerun.js +2 -1
  150. package/lib/result.js +2 -1
  151. package/lib/step/base.js +3 -2
  152. package/lib/step/config.js +15 -2
  153. package/lib/step/record.js +2 -2
  154. package/lib/store.js +72 -3
  155. package/lib/translation.js +2 -1
  156. package/lib/utils/mask_data.js +2 -1
  157. package/lib/utils/pluginParser.js +151 -0
  158. package/lib/utils/trace.js +297 -0
  159. package/lib/utils.js +77 -3
  160. package/lib/workers.js +63 -25
  161. package/package.json +19 -13
  162. package/typings/index.d.ts +19 -5
  163. package/docs/webapi/amOnPage.mustache +0 -11
  164. package/docs/webapi/appendField.mustache +0 -11
  165. package/docs/webapi/attachFile.mustache +0 -12
  166. package/docs/webapi/blur.mustache +0 -18
  167. package/docs/webapi/checkOption.mustache +0 -13
  168. package/docs/webapi/clearCookie.mustache +0 -9
  169. package/docs/webapi/clearField.mustache +0 -9
  170. package/docs/webapi/click.mustache +0 -29
  171. package/docs/webapi/clickLink.mustache +0 -8
  172. package/docs/webapi/closeCurrentTab.mustache +0 -7
  173. package/docs/webapi/closeOtherTabs.mustache +0 -8
  174. package/docs/webapi/dontSee.mustache +0 -11
  175. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  176. package/docs/webapi/dontSeeCookie.mustache +0 -8
  177. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  178. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  179. package/docs/webapi/dontSeeElement.mustache +0 -8
  180. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  181. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  182. package/docs/webapi/dontSeeInField.mustache +0 -11
  183. package/docs/webapi/dontSeeInSource.mustache +0 -8
  184. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  185. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  186. package/docs/webapi/doubleClick.mustache +0 -13
  187. package/docs/webapi/downloadFile.mustache +0 -12
  188. package/docs/webapi/dragAndDrop.mustache +0 -9
  189. package/docs/webapi/dragSlider.mustache +0 -11
  190. package/docs/webapi/executeAsyncScript.mustache +0 -24
  191. package/docs/webapi/executeScript.mustache +0 -26
  192. package/docs/webapi/fillField.mustache +0 -16
  193. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  194. package/docs/webapi/focus.mustache +0 -13
  195. package/docs/webapi/forceClick.mustache +0 -28
  196. package/docs/webapi/forceRightClick.mustache +0 -18
  197. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  198. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  199. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  200. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  201. package/docs/webapi/grabCookie.mustache +0 -11
  202. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  203. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  204. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  205. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  206. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  207. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  208. package/docs/webapi/grabGeoLocation.mustache +0 -8
  209. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  210. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  211. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  212. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  213. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  214. package/docs/webapi/grabPopupText.mustache +0 -5
  215. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  216. package/docs/webapi/grabSource.mustache +0 -8
  217. package/docs/webapi/grabTextFrom.mustache +0 -10
  218. package/docs/webapi/grabTextFromAll.mustache +0 -9
  219. package/docs/webapi/grabTitle.mustache +0 -8
  220. package/docs/webapi/grabValueFrom.mustache +0 -9
  221. package/docs/webapi/grabValueFromAll.mustache +0 -8
  222. package/docs/webapi/grabWebElement.mustache +0 -9
  223. package/docs/webapi/grabWebElements.mustache +0 -9
  224. package/docs/webapi/moveCursorTo.mustache +0 -12
  225. package/docs/webapi/openNewTab.mustache +0 -7
  226. package/docs/webapi/pressKey.mustache +0 -12
  227. package/docs/webapi/pressKeyDown.mustache +0 -12
  228. package/docs/webapi/pressKeyUp.mustache +0 -12
  229. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  230. package/docs/webapi/refreshPage.mustache +0 -6
  231. package/docs/webapi/resizeWindow.mustache +0 -6
  232. package/docs/webapi/rightClick.mustache +0 -14
  233. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  234. package/docs/webapi/saveScreenshot.mustache +0 -12
  235. package/docs/webapi/say.mustache +0 -10
  236. package/docs/webapi/scrollIntoView.mustache +0 -11
  237. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  238. package/docs/webapi/scrollPageToTop.mustache +0 -6
  239. package/docs/webapi/scrollTo.mustache +0 -12
  240. package/docs/webapi/see.mustache +0 -11
  241. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  242. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  243. package/docs/webapi/seeCookie.mustache +0 -8
  244. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  245. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  246. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  247. package/docs/webapi/seeElement.mustache +0 -8
  248. package/docs/webapi/seeElementInDOM.mustache +0 -8
  249. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  250. package/docs/webapi/seeInField.mustache +0 -12
  251. package/docs/webapi/seeInPopup.mustache +0 -8
  252. package/docs/webapi/seeInSource.mustache +0 -7
  253. package/docs/webapi/seeInTitle.mustache +0 -8
  254. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  255. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  256. package/docs/webapi/seeTextEquals.mustache +0 -9
  257. package/docs/webapi/seeTitleEquals.mustache +0 -8
  258. package/docs/webapi/seeTraffic.mustache +0 -36
  259. package/docs/webapi/selectOption.mustache +0 -21
  260. package/docs/webapi/setCookie.mustache +0 -16
  261. package/docs/webapi/setGeoLocation.mustache +0 -12
  262. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  263. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  264. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  265. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  266. package/docs/webapi/switchTo.mustache +0 -9
  267. package/docs/webapi/switchToNextTab.mustache +0 -10
  268. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  269. package/docs/webapi/type.mustache +0 -21
  270. package/docs/webapi/uncheckOption.mustache +0 -13
  271. package/docs/webapi/wait.mustache +0 -8
  272. package/docs/webapi/waitForClickable.mustache +0 -11
  273. package/docs/webapi/waitForCookie.mustache +0 -9
  274. package/docs/webapi/waitForDetached.mustache +0 -10
  275. package/docs/webapi/waitForDisabled.mustache +0 -6
  276. package/docs/webapi/waitForElement.mustache +0 -11
  277. package/docs/webapi/waitForEnabled.mustache +0 -6
  278. package/docs/webapi/waitForFunction.mustache +0 -17
  279. package/docs/webapi/waitForInvisible.mustache +0 -10
  280. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  281. package/docs/webapi/waitForText.mustache +0 -13
  282. package/docs/webapi/waitForValue.mustache +0 -10
  283. package/docs/webapi/waitForVisible.mustache +0 -10
  284. package/docs/webapi/waitInUrl.mustache +0 -9
  285. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  286. package/docs/webapi/waitToHide.mustache +0 -10
  287. package/docs/webapi/waitUrlEquals.mustache +0 -10
  288. package/lib/helper/AI.js +0 -214
  289. package/lib/listener/enhancedGlobalRetry.js +0 -110
  290. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  291. package/lib/plugin/htmlReporter.js +0 -3648
  292. package/lib/plugin/stepByStepReport.js +0 -427
  293. package/lib/plugin/subtitles.js +0 -89
  294. package/lib/retryCoordinator.js +0 -207
  295. package/typings/promiseBasedTypes.d.ts +0 -9469
  296. package/typings/types.d.ts +0 -11402
@@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry'
7
7
  import Locator from '../locator.js'
8
8
  import recorder from '../recorder.js'
9
9
  import store from '../store.js'
10
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
10
11
  import { includes as stringIncludes } from '../assert/include.js'
11
12
  import { urlEquals, equals } from '../assert/equal.js'
12
13
  import { empty } from '../assert/empty.js'
@@ -23,7 +24,11 @@ import {
23
24
  clearString,
24
25
  requireWithFallback,
25
26
  normalizeSpacesInString,
27
+ normalizePath,
28
+ resolveUrl,
26
29
  relativeDir,
30
+ getMimeType,
31
+ base64EncodeFile,
27
32
  } from '../utils.js'
28
33
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
29
34
  import ElementNotFound from './errors/ElementNotFound.js'
@@ -31,17 +36,16 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
31
36
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
32
37
  import Popup from './extras/Popup.js'
33
38
  import Console from './extras/Console.js'
34
- import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
+ import { findReact, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
40
+ import { dropFile } from './scripts/dropFile.js'
35
41
  import WebElement from '../element/WebElement.js'
42
+ import { selectElement } from './extras/elementSelection.js'
43
+ import { fillRichEditor } from './extras/richTextEditor.js'
36
44
 
37
45
  let playwright
38
46
  let perfTiming
39
47
  let defaultSelectorEnginesInitialized = false
40
48
 
41
- // Use global object to track selector registration across workers
42
- if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
43
- global.__playwrightSelectorsRegistered = false
44
- }
45
49
 
46
50
  const popupStore = new Popup()
47
51
  const consoleLogStore = new Console()
@@ -442,7 +446,7 @@ class Playwright extends Helper {
442
446
  this.options.recordVideo = { size }
443
447
  }
444
448
  if (this.options.recordVideo && !this.options.recordVideo.dir) {
445
- this.options.recordVideo.dir = `${global.output_dir}/videos/`
449
+ this.options.recordVideo.dir = `${store.outputDir}/videos/`
446
450
  }
447
451
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
448
452
  this.isElectron = this.options.browser === 'electron'
@@ -504,18 +508,18 @@ class Playwright extends Helper {
504
508
  try {
505
509
  // Always wrap in try-catch since selectors might be registered globally across workers
506
510
  // Check global flag to avoid re-registration in worker processes
507
- if (!global.__playwrightSelectorsRegistered) {
511
+ if (!defaultSelectorEnginesInitialized) {
508
512
  try {
509
513
  await playwright.selectors.register('__value', createValueEngine)
510
514
  await playwright.selectors.register('__disabled', createDisabledEngine)
511
- global.__playwrightSelectorsRegistered = true
515
+ defaultSelectorEnginesInitialized = true
512
516
  defaultSelectorEnginesInitialized = true
513
517
  } catch (e) {
514
518
  if (!e.message.includes('already registered')) {
515
519
  throw e
516
520
  }
517
521
  // Selector already registered globally by another worker
518
- global.__playwrightSelectorsRegistered = true
522
+ defaultSelectorEnginesInitialized = true
519
523
  defaultSelectorEnginesInitialized = true
520
524
  }
521
525
  } else {
@@ -608,7 +612,7 @@ class Playwright extends Helper {
608
612
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
609
613
  if (this.options.recordHar) {
610
614
  const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
611
- const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
615
+ const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
612
616
  const dir = path.dirname(fileName)
613
617
  if (!fileExists(dir)) fs.mkdirSync(dir)
614
618
  this.options.recordHar.path = fileName
@@ -751,6 +755,11 @@ class Playwright extends Helper {
751
755
  }
752
756
 
753
757
  async _afterSuite() {
758
+ // Reset leftover test-level cleanup state (e.g. screenshot failures)
759
+ // so only errors from this suite teardown are evaluated below.
760
+ this.hasCleanupError = false
761
+ this.testFailures = []
762
+
754
763
  // Stop browser after suite completes
755
764
  // For restart strategies: stop after each suite
756
765
  // For session mode (restart:false): stop after the last suite
@@ -1490,8 +1499,23 @@ class Playwright extends Helper {
1490
1499
  *
1491
1500
  */
1492
1501
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1493
- const el = await this._locateElement(locator)
1494
- assertElementExists(el, locator)
1502
+ let context = null
1503
+ if (typeof offsetX !== 'number') {
1504
+ context = offsetX
1505
+ offsetX = 0
1506
+ }
1507
+
1508
+ let el
1509
+ if (context) {
1510
+ const contextEls = await this._locate(context)
1511
+ assertElementExists(contextEls, context, 'Context element')
1512
+ el = await findElements.call(this, contextEls[0], locator)
1513
+ assertElementExists(el, locator)
1514
+ el = el[0]
1515
+ } else {
1516
+ el = await this._locateElement(locator)
1517
+ assertElementExists(el, locator)
1518
+ }
1495
1519
 
1496
1520
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
1497
1521
  const { x, y } = await clickablePoint(el)
@@ -1615,7 +1639,7 @@ class Playwright extends Helper {
1615
1639
  * @returns Promise<void>
1616
1640
  */
1617
1641
  async replayFromHar(harFilePath, opts) {
1618
- const file = path.join(global.codecept_dir, harFilePath)
1642
+ const file = path.join(store.codeceptDir, harFilePath)
1619
1643
 
1620
1644
  if (!fileExists(file)) {
1621
1645
  throw new Error(`File at ${file} cannot be found on local system`)
@@ -1759,8 +1783,7 @@ class Playwright extends Helper {
1759
1783
  if (elements.length === 0) {
1760
1784
  throw new ElementNotFound(locator, 'Element', 'was not found')
1761
1785
  }
1762
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1763
- return elements[0]
1786
+ return selectElement(elements, locator, this)
1764
1787
  }
1765
1788
 
1766
1789
  /**
@@ -1775,8 +1798,7 @@ class Playwright extends Helper {
1775
1798
  const context = providedContext || (await this._getContext())
1776
1799
  const els = await findCheckable.call(this, locator, context)
1777
1800
  assertElementExists(els[0], locator, 'Checkbox or radio')
1778
- if (this.options.strict) assertOnlyOneElement(els, locator)
1779
- return els[0]
1801
+ return selectElement(els, locator, this)
1780
1802
  }
1781
1803
 
1782
1804
  /**
@@ -1944,8 +1966,15 @@ class Playwright extends Helper {
1944
1966
  * {{> seeElement }}
1945
1967
  *
1946
1968
  */
1947
- async seeElement(locator) {
1948
- let els = await this._locate(locator)
1969
+ async seeElement(locator, context = null) {
1970
+ let els
1971
+ if (context) {
1972
+ const contextEls = await this._locate(context)
1973
+ assertElementExists(contextEls, context, 'Context element')
1974
+ els = await findElements.call(this, contextEls[0], locator)
1975
+ } else {
1976
+ els = await this._locate(locator)
1977
+ }
1949
1978
  els = await Promise.all(els.map(el => el.isVisible()))
1950
1979
  try {
1951
1980
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
@@ -1958,8 +1987,15 @@ class Playwright extends Helper {
1958
1987
  * {{> dontSeeElement }}
1959
1988
  *
1960
1989
  */
1961
- async dontSeeElement(locator) {
1962
- let els = await this._locate(locator)
1990
+ async dontSeeElement(locator, context = null) {
1991
+ let els
1992
+ if (context) {
1993
+ const contextEls = await this._locate(context)
1994
+ assertElementExists(contextEls, context, 'Context element')
1995
+ els = await findElements.call(this, contextEls[0], locator)
1996
+ } else {
1997
+ els = await this._locate(locator)
1998
+ }
1963
1999
  els = await Promise.all(els.map(el => el.isVisible()))
1964
2000
  try {
1965
2001
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
@@ -2014,7 +2050,7 @@ class Playwright extends Helper {
2014
2050
  const filePath = await download.path()
2015
2051
  fileName = fileName || `downloads/${path.basename(filePath)}`
2016
2052
 
2017
- const downloadPath = path.join(global.output_dir, fileName)
2053
+ const downloadPath = path.join(store.outputDir, fileName)
2018
2054
  if (!fs.existsSync(path.dirname(downloadPath))) {
2019
2055
  fs.mkdirSync(path.dirname(downloadPath), '0777')
2020
2056
  }
@@ -2198,6 +2234,7 @@ class Playwright extends Helper {
2198
2234
  * {{> pressKeyWithKeyNormalization }}
2199
2235
  */
2200
2236
  async pressKey(key) {
2237
+ await checkFocusBeforePressKey(this, key)
2201
2238
  const modifiers = []
2202
2239
  if (Array.isArray(key)) {
2203
2240
  for (let k of key) {
@@ -2226,6 +2263,8 @@ class Playwright extends Helper {
2226
2263
  * {{> type }}
2227
2264
  */
2228
2265
  async type(keys, delay = null) {
2266
+ await checkFocusBeforeType(this)
2267
+
2229
2268
  // Always use page.keyboard.type for any string (including single character and national characters).
2230
2269
  if (!Array.isArray(keys)) {
2231
2270
  keys = keys.toString()
@@ -2245,45 +2284,33 @@ class Playwright extends Helper {
2245
2284
  * {{> fillField }}
2246
2285
  *
2247
2286
  */
2248
- async fillField(field, value) {
2249
- const els = await findFields.call(this, field)
2287
+ async fillField(field, value, context = null) {
2288
+ const els = await findFields.call(this, field, context)
2250
2289
  assertElementExists(els, field, 'Field')
2251
- if (this.options.strict) assertOnlyOneElement(els, field)
2252
- const el = els[0]
2290
+ const el = selectElement(els, field, this)
2291
+
2292
+ await highlightActiveElement.call(this, el)
2293
+
2294
+ if (await fillRichEditor(this, el, value)) {
2295
+ return this._waitForAction()
2296
+ }
2253
2297
 
2254
2298
  await el.clear()
2255
2299
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
2256
2300
 
2257
- await highlightActiveElement.call(this, el)
2258
-
2259
2301
  await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2260
2302
 
2261
2303
  return this._waitForAction()
2262
2304
  }
2263
2305
 
2264
2306
  /**
2265
- * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
2266
- *
2267
- *
2268
- * Examples:
2269
- *
2270
- * ```js
2271
- * I.clearField('.text-area')
2272
- *
2273
- * // if this doesn't work use force option
2274
- * I.clearField('#submit', { force: true })
2275
- * ```
2276
- * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
2277
- *
2278
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
2279
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
2307
+ * {{> clearField }}
2280
2308
  */
2281
- async clearField(locator, options = {}) {
2282
- const els = await findFields.call(this, locator)
2309
+ async clearField(locator, context = null) {
2310
+ const els = await findFields.call(this, locator, context)
2283
2311
  assertElementExists(els, locator, 'Field to clear')
2284
- if (this.options.strict) assertOnlyOneElement(els, locator)
2285
2312
 
2286
- const el = els[0]
2313
+ const el = selectElement(els, locator, this)
2287
2314
 
2288
2315
  await highlightActiveElement.call(this, el)
2289
2316
 
@@ -2295,76 +2322,101 @@ class Playwright extends Helper {
2295
2322
  /**
2296
2323
  * {{> appendField }}
2297
2324
  */
2298
- async appendField(field, value) {
2299
- const els = await findFields.call(this, field)
2325
+ async appendField(field, value, context = null) {
2326
+ const els = await findFields.call(this, field, context)
2300
2327
  assertElementExists(els, field, 'Field')
2301
- if (this.options.strict) assertOnlyOneElement(els, field)
2302
- await highlightActiveElement.call(this, els[0])
2303
- await els[0].press('End')
2304
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
2328
+ const el = selectElement(els, field, this)
2329
+ await highlightActiveElement.call(this, el)
2330
+ await el.press('End')
2331
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2305
2332
  return this._waitForAction()
2306
2333
  }
2307
2334
 
2308
2335
  /**
2309
2336
  * {{> seeInField }}
2310
2337
  */
2311
- async seeInField(field, value) {
2338
+ async seeInField(field, value, context = null) {
2312
2339
  const _value = typeof value === 'boolean' ? value : value.toString()
2313
- return proceedSeeInField.call(this, 'assert', field, _value)
2340
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
2314
2341
  }
2315
2342
 
2316
2343
  /**
2317
2344
  * {{> dontSeeInField }}
2318
2345
  */
2319
- async dontSeeInField(field, value) {
2346
+ async dontSeeInField(field, value, context = null) {
2320
2347
  const _value = typeof value === 'boolean' ? value : value.toString()
2321
- return proceedSeeInField.call(this, 'negate', field, _value)
2348
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
2322
2349
  }
2323
2350
 
2324
2351
  /**
2325
2352
  * {{> attachFile }}
2326
2353
  *
2327
2354
  */
2328
- async attachFile(locator, pathToFile) {
2329
- const file = path.join(global.codecept_dir, pathToFile)
2355
+ async attachFile(locator, pathToFile, context = null) {
2356
+ const file = path.join(store.codeceptDir, pathToFile)
2330
2357
 
2331
2358
  if (!fileExists(file)) {
2332
2359
  throw new Error(`File at ${file} can not be found on local system`)
2333
2360
  }
2334
- const els = await findFields.call(this, locator)
2335
- assertElementExists(els, locator, 'Field')
2336
- await els[0].setInputFiles(file)
2361
+ const els = await findFields.call(this, locator, context)
2362
+ if (els.length) {
2363
+ const el = selectElement(els, locator, this)
2364
+ const tag = await el.evaluate(el => el.tagName)
2365
+ const type = await el.evaluate(el => el.type)
2366
+ if (tag === 'INPUT' && type === 'file') {
2367
+ await el.setInputFiles(file)
2368
+ return this._waitForAction()
2369
+ }
2370
+ }
2371
+
2372
+ const targetEls = els.length ? els : await this._locate(locator)
2373
+ assertElementExists(targetEls, locator, 'Element')
2374
+ const el = selectElement(targetEls, locator, this)
2375
+ const fileData = {
2376
+ base64Content: base64EncodeFile(file),
2377
+ fileName: path.basename(file),
2378
+ mimeType: getMimeType(path.basename(file)),
2379
+ }
2380
+ await el.evaluate(dropFile, fileData)
2337
2381
  return this._waitForAction()
2338
2382
  }
2339
2383
 
2340
2384
  /**
2341
2385
  * {{> selectOption }}
2342
2386
  */
2343
- async selectOption(select, option) {
2344
- const context = await this.context
2387
+ async selectOption(select, option, context = null) {
2388
+ const pageContext = await this.context
2345
2389
  const matchedLocator = new Locator(select)
2346
2390
 
2391
+ let contextEl
2392
+ if (context) {
2393
+ const contextEls = await this._locate(context)
2394
+ assertElementExists(contextEls, context, 'Context element')
2395
+ contextEl = contextEls[0]
2396
+ }
2397
+
2347
2398
  // Strict locator
2348
2399
  if (!matchedLocator.isFuzzy()) {
2349
2400
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2350
- const els = await this._locate(matchedLocator)
2401
+ const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2351
2402
  assertElementExists(els, select, 'Selectable element')
2352
- return proceedSelect.call(this, context, els[0], option)
2403
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2353
2404
  }
2354
2405
 
2355
2406
  // Fuzzy: try combobox
2356
2407
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2357
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
2358
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2408
+ const comboboxSearchCtx = contextEl || pageContext
2409
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2410
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2359
2411
 
2360
2412
  // Fuzzy: try listbox
2361
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
2362
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2413
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2414
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2363
2415
 
2364
2416
  // Fuzzy: try native select
2365
- els = await findFields.call(this, select)
2417
+ els = await findFields.call(this, select, context)
2366
2418
  assertElementExists(els, select, 'Selectable element')
2367
- return proceedSelect.call(this, context, els[0], option)
2419
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2368
2420
  }
2369
2421
 
2370
2422
  /**
@@ -2412,7 +2464,7 @@ class Playwright extends Helper {
2412
2464
  const currentUrl = await this._getPageUrl()
2413
2465
  const baseUrl = this.options.url || 'http://localhost'
2414
2466
  const actualPath = new URL(currentUrl, baseUrl).pathname
2415
- return equals('url path').assert(path, actualPath)
2467
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
2416
2468
  }
2417
2469
 
2418
2470
  /**
@@ -2422,7 +2474,7 @@ class Playwright extends Helper {
2422
2474
  const currentUrl = await this._getPageUrl()
2423
2475
  const baseUrl = this.options.url || 'http://localhost'
2424
2476
  const actualPath = new URL(currentUrl, baseUrl).pathname
2425
- return equals('url path').negate(path, actualPath)
2477
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
2426
2478
  }
2427
2479
 
2428
2480
  /**
@@ -2627,8 +2679,11 @@ class Playwright extends Helper {
2627
2679
  * @returns {Promise<any>}
2628
2680
  */
2629
2681
  async executeScript(fn, arg) {
2682
+ if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
2683
+ if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
2684
+ return arg.evaluate(fn)
2685
+ }
2630
2686
  if (this.context && this.context.constructor.name === 'FrameLocator') {
2631
- // switching to iframe context
2632
2687
  return this.context.locator(':root').evaluate(fn, arg)
2633
2688
  }
2634
2689
  return this.page.evaluate.apply(this.page, [fn, arg])
@@ -2658,15 +2713,12 @@ class Playwright extends Helper {
2658
2713
  *
2659
2714
  */
2660
2715
  async grabTextFrom(locator) {
2661
- // Handle role locators with text/exact options
2662
- if (isRoleLocatorObject(locator)) {
2663
- const elements = await handleRoleLocator(this.page, locator)
2664
- if (elements && elements.length > 0) {
2665
- const text = await elements[0].textContent()
2666
- assertElementExists(text, JSON.stringify(locator))
2667
- this.debugSection('Text', text)
2668
- return text
2669
- }
2716
+ const roleElements = await handleRoleLocator(this.page, locator)
2717
+ if (roleElements && roleElements.length > 0) {
2718
+ const text = await roleElements[0].textContent()
2719
+ assertElementExists(text, JSON.stringify(locator))
2720
+ this.debugSection('Text', text)
2721
+ return text
2670
2722
  }
2671
2723
 
2672
2724
  const locatorObj = new Locator(locator, 'css')
@@ -2894,7 +2946,7 @@ class Playwright extends Helper {
2894
2946
  const els = await this._locate(matchedLocator)
2895
2947
  assertElementExists(els, locator)
2896
2948
  const snapshot = await els[0].ariaSnapshot()
2897
- this.debugSection('Aria Snapshot', snapshot)
2949
+ this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
2898
2950
  return snapshot
2899
2951
  }
2900
2952
 
@@ -3382,6 +3434,7 @@ class Playwright extends Helper {
3382
3434
  */
3383
3435
  async waitInUrl(urlPart, sec = null) {
3384
3436
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3437
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3385
3438
 
3386
3439
  return this.page
3387
3440
  .waitForFunction(
@@ -3389,13 +3442,13 @@ class Playwright extends Helper {
3389
3442
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
3390
3443
  return currUrl.indexOf(urlPart) > -1
3391
3444
  },
3392
- urlPart,
3445
+ expectedUrl,
3393
3446
  { timeout: waitTimeout },
3394
3447
  )
3395
3448
  .catch(async e => {
3396
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
3449
+ const currUrl = await this._getPageUrl()
3397
3450
  if (/Timeout/i.test(e.message)) {
3398
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
3451
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
3399
3452
  } else {
3400
3453
  throw e
3401
3454
  }
@@ -3407,26 +3460,46 @@ class Playwright extends Helper {
3407
3460
  */
3408
3461
  async waitUrlEquals(urlPart, sec = null) {
3409
3462
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3410
-
3411
- const baseUrl = this.options.url
3412
- let expectedUrl = urlPart
3413
- if (urlPart.indexOf('http') < 0) {
3414
- expectedUrl = baseUrl + urlPart
3415
- }
3463
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3416
3464
 
3417
3465
  try {
3418
3466
  await this.page.waitForURL(
3419
- url => url.href.includes(expectedUrl),
3467
+ url => url.href === expectedUrl,
3420
3468
  { timeout: waitTimeout },
3421
3469
  )
3422
3470
  } catch (e) {
3423
3471
  const currUrl = await this._getPageUrl()
3424
3472
  if (/Timeout/i.test(e.message)) {
3425
- if (!currUrl.includes(expectedUrl)) {
3426
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3427
- } else {
3428
- throw new Error(`expected url not loaded, error message: ${e.message}`)
3429
- }
3473
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3474
+ } else {
3475
+ throw e
3476
+ }
3477
+ }
3478
+ }
3479
+
3480
+ /**
3481
+ * {{> waitCurrentPathEquals }}
3482
+ */
3483
+ async waitCurrentPathEquals(path, sec = null) {
3484
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3485
+ const normalizedPath = normalizePath(path)
3486
+
3487
+ try {
3488
+ await this.page.waitForFunction(
3489
+ expectedPath => {
3490
+ const actualPath = window.location.pathname
3491
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
3492
+ return normalizePath(actualPath) === expectedPath
3493
+ },
3494
+ normalizedPath,
3495
+ { timeout: waitTimeout },
3496
+ )
3497
+ } catch (e) {
3498
+ const currentUrl = await this._getPageUrl()
3499
+ const baseUrl = this.options.url || 'http://localhost'
3500
+ const actualPath = new URL(currentUrl, baseUrl).pathname
3501
+ if (/Timeout/i.test(e.message)) {
3502
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
3430
3503
  } else {
3431
3504
  throw e
3432
3505
  }
@@ -4112,9 +4185,15 @@ class Playwright extends Helper {
4112
4185
 
4113
4186
  export default Playwright
4114
4187
 
4115
- function buildLocatorString(locator) {
4188
+ export function buildLocatorString(locator) {
4116
4189
  if (locator.isXPath()) {
4117
- return `xpath=${locator.value}`
4190
+ // Make XPath relative so it works correctly within scoped contexts (e.g. within()).
4191
+ // Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
4192
+ // but only when the selector starts with "/". Locator methods like at() wrap XPath in
4193
+ // parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
4194
+ // We fix this by prepending "." before the first "//" that follows any leading parentheses.
4195
+ const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
4196
+ return `xpath=${value}`
4118
4197
  }
4119
4198
  if (locator.isShadow()) {
4120
4199
  // Convert shadow locator to CSS with >> chaining operator
@@ -4125,25 +4204,22 @@ function buildLocatorString(locator) {
4125
4204
  return locator.simplify()
4126
4205
  }
4127
4206
 
4128
- /**
4129
- * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4130
- */
4131
- function isRoleLocatorObject(locator) {
4132
- return locator && typeof locator === 'object' && locator.role && !locator.type
4133
- }
4134
-
4135
4207
  /**
4136
4208
  * Handles role locator objects by converting them to Playwright's getByRole() API
4209
+ * Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
4137
4210
  * Returns elements array if role locator, null otherwise
4138
4211
  */
4139
4212
  async function handleRoleLocator(context, locator) {
4140
- if (!isRoleLocatorObject(locator)) return null
4213
+ const loc = new Locator(locator)
4214
+ if (!loc.isRole()) return null
4141
4215
 
4216
+ const roleObj = loc.locator || {}
4142
4217
  const options = {}
4143
- if (locator.text) options.name = locator.text
4144
- if (locator.exact !== undefined) options.exact = locator.exact
4218
+ if (roleObj.text) options.name = roleObj.text
4219
+ if (roleObj.name) options.name = roleObj.name
4220
+ if (roleObj.exact !== undefined) options.exact = roleObj.exact
4145
4221
 
4146
- return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4222
+ return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
4147
4223
  }
4148
4224
 
4149
4225
  async function findByRole(context, locator) {
@@ -4155,13 +4231,10 @@ async function findByRole(context, locator) {
4155
4231
  }
4156
4232
 
4157
4233
  async function findElements(matcher, locator) {
4158
- // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4159
4234
  const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4160
- const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4161
4235
  const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4162
4236
 
4163
4237
  if (isReactLocator) return findReact(matcher, locator)
4164
- if (isVueLocator) return findVue(matcher, locator)
4165
4238
  if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4166
4239
 
4167
4240
  // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
@@ -4177,7 +4250,6 @@ async function findElements(matcher, locator) {
4177
4250
 
4178
4251
  async function findElement(matcher, locator) {
4179
4252
  if (locator.react) return findReact(matcher, locator)
4180
- if (locator.vue) return findVue(matcher, locator)
4181
4253
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4182
4254
 
4183
4255
  locator = new Locator(locator, 'css')
@@ -4212,16 +4284,22 @@ async function proceedClick(locator, context = null, options = {}) {
4212
4284
  assertElementExists(els, locator, 'Clickable element')
4213
4285
  }
4214
4286
 
4215
- await highlightActiveElement.call(this, els[0])
4216
- if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
4287
+ const opts = store.currentStep?.opts
4288
+ let element
4289
+ if (opts?.elementIndex != null) {
4290
+ element = selectElement(els, locator, this)
4291
+ } else {
4292
+ const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
4293
+ if (strict) assertOnlyOneElement(els, locator, this)
4294
+ element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4295
+ }
4296
+
4297
+ await highlightActiveElement.call(this, element)
4298
+ if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
4217
4299
 
4218
- /*
4219
- using the force true options itself but instead dispatching a click
4220
- */
4221
4300
  if (options.force) {
4222
- await els[0].dispatchEvent('click')
4301
+ await element.dispatchEvent('click')
4223
4302
  } else {
4224
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4225
4303
  await element.click(options)
4226
4304
  }
4227
4305
  const promises = []
@@ -4238,7 +4316,6 @@ async function findClickable(matcher, locator) {
4238
4316
 
4239
4317
  if (!matchedLocator.isFuzzy()) {
4240
4318
  const els = await findElements.call(this, matcher, matchedLocator)
4241
- if (this.options.strict) assertOnlyOneElement(els, locator)
4242
4319
  return els
4243
4320
  }
4244
4321
 
@@ -4247,42 +4324,27 @@ async function findClickable(matcher, locator) {
4247
4324
 
4248
4325
  try {
4249
4326
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4250
- if (els.length) {
4251
- if (this.options.strict) assertOnlyOneElement(els, locator)
4252
- return els
4253
- }
4327
+ if (els.length) return els
4254
4328
  } catch (err) {
4255
4329
  // getByRole not supported or failed
4256
4330
  }
4257
4331
 
4258
4332
  try {
4259
4333
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4260
- if (els.length) {
4261
- if (this.options.strict) assertOnlyOneElement(els, locator)
4262
- return els
4263
- }
4334
+ if (els.length) return els
4264
4335
  } catch (err) {
4265
4336
  // getByRole not supported or failed
4266
4337
  }
4267
4338
 
4268
4339
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4269
- if (els.length) {
4270
- if (this.options.strict) assertOnlyOneElement(els, locator)
4271
- return els
4272
- }
4340
+ if (els.length) return els
4273
4341
 
4274
4342
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4275
- if (els.length) {
4276
- if (this.options.strict) assertOnlyOneElement(els, locator)
4277
- return els
4278
- }
4343
+ if (els.length) return els
4279
4344
 
4280
4345
  try {
4281
4346
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4282
- if (els.length) {
4283
- if (this.options.strict) assertOnlyOneElement(els, locator)
4284
- return els
4285
- }
4347
+ if (els.length) return els
4286
4348
  } catch (err) {
4287
4349
  // Do nothing
4288
4350
  }
@@ -4355,34 +4417,42 @@ async function proceedIsChecked(assertType, option) {
4355
4417
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
4356
4418
  }
4357
4419
 
4358
- async function findFields(locator) {
4359
- // Handle role locators with text/exact options
4360
- if (isRoleLocatorObject(locator)) {
4361
- const page = await this.page
4362
- const roleElements = await handleRoleLocator(page, locator)
4363
- if (roleElements) return roleElements
4420
+ async function findFields(locator, context = null) {
4421
+ let contextEl
4422
+ if (context) {
4423
+ const contextEls = await this._locate(context)
4424
+ assertElementExists(contextEls, context, 'Context element')
4425
+ contextEl = contextEls[0]
4364
4426
  }
4365
4427
 
4428
+ const locateFn = contextEl
4429
+ ? loc => findElements.call(this, contextEl, loc)
4430
+ : loc => this._locate(loc)
4431
+
4432
+ const matcher = contextEl || (await this.page)
4433
+ const roleElements = await handleRoleLocator(matcher, locator)
4434
+ if (roleElements) return roleElements
4435
+
4366
4436
  const matchedLocator = new Locator(locator)
4367
4437
  if (!matchedLocator.isFuzzy()) {
4368
- return this._locate(matchedLocator)
4438
+ return locateFn(matchedLocator)
4369
4439
  }
4370
4440
  const literal = xpathLocator.literal(locator)
4371
4441
 
4372
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
4442
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
4373
4443
  if (els.length) {
4374
4444
  return els
4375
4445
  }
4376
4446
 
4377
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
4447
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
4378
4448
  if (els.length) {
4379
4449
  return els
4380
4450
  }
4381
- els = await this._locate({ xpath: Locator.field.byName(literal) })
4451
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
4382
4452
  if (els.length) {
4383
4453
  return els
4384
4454
  }
4385
- return this._locate({ css: locator })
4455
+ return locateFn({ css: locator })
4386
4456
  }
4387
4457
 
4388
4458
  async function proceedSelect(context, el, option) {
@@ -4431,8 +4501,8 @@ async function proceedSelect(context, el, option) {
4431
4501
  return this._waitForAction()
4432
4502
  }
4433
4503
 
4434
- async function proceedSeeInField(assertType, field, value) {
4435
- const els = await findFields.call(this, field)
4504
+ async function proceedSeeInField(assertType, field, value, context) {
4505
+ const els = await findFields.call(this, field, context)
4436
4506
  assertElementExists(els, field, 'Field')
4437
4507
  const el = els[0]
4438
4508
  const tag = await el.evaluate(e => e.tagName)
@@ -4546,9 +4616,10 @@ function assertElementExists(res, locator, prefix, suffix) {
4546
4616
  }
4547
4617
  }
4548
4618
 
4549
- function assertOnlyOneElement(elements, locator) {
4619
+ function assertOnlyOneElement(elements, locator, helper) {
4550
4620
  if (elements.length > 1) {
4551
- throw new MultipleElementsFound(locator, elements)
4621
+ const webElements = elements.map(el => new WebElement(el, helper))
4622
+ throw new MultipleElementsFound(locator, webElements)
4552
4623
  }
4553
4624
  }
4554
4625
 
@@ -4757,7 +4828,7 @@ async function refreshContextSession() {
4757
4828
 
4758
4829
  function saveVideoForPage(page, name) {
4759
4830
  if (!page.video()) return null
4760
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4831
+ const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4761
4832
  page
4762
4833
  .video()
4763
4834
  .saveAs(fileName)
@@ -4774,7 +4845,7 @@ async function saveTraceForContext(context, name) {
4774
4845
  if (!context) return
4775
4846
  if (!context.tracing) return
4776
4847
  try {
4777
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4848
+ const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4778
4849
  await context.tracing.stop({ path: fileName })
4779
4850
  return fileName
4780
4851
  } catch (err) {