codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20

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 (294) 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 +1187 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +159 -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/examples.md +161 -0
  30. package/docs/heal.md +213 -0
  31. package/docs/helpers/ApiDataFactory.md +267 -0
  32. package/docs/helpers/Appium.md +1405 -0
  33. package/docs/helpers/Detox.md +665 -0
  34. package/docs/helpers/ExpectHelper.md +275 -0
  35. package/docs/helpers/FileSystem.md +152 -0
  36. package/docs/helpers/GraphQL.md +152 -0
  37. package/docs/helpers/GraphQLDataFactory.md +226 -0
  38. package/docs/helpers/JSONResponse.md +255 -0
  39. package/docs/helpers/Mochawesome.md +8 -0
  40. package/docs/helpers/MockRequest.md +377 -0
  41. package/docs/helpers/MockServer.md +212 -0
  42. package/docs/helpers/Playwright.md +2969 -0
  43. package/docs/helpers/Polly.md +44 -0
  44. package/docs/helpers/Protractor.md +1769 -0
  45. package/docs/helpers/Puppeteer-firefox.md +86 -0
  46. package/docs/helpers/Puppeteer.md +2690 -0
  47. package/docs/helpers/REST.md +289 -0
  48. package/docs/helpers/SoftExpectHelper.md +352 -0
  49. package/docs/helpers/WebDriver.md +2682 -0
  50. package/docs/hooks.md +339 -0
  51. package/docs/index.md +111 -0
  52. package/docs/installation.md +83 -0
  53. package/docs/internal-api.md +265 -0
  54. package/docs/internal-test-server.md +89 -0
  55. package/docs/locators.md +355 -0
  56. package/docs/mcp.md +485 -0
  57. package/docs/migration-4.md +556 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +585 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins.md +866 -0
  63. package/docs/puppeteer.md +314 -0
  64. package/docs/quickstart.md +120 -0
  65. package/docs/react.md +70 -0
  66. package/docs/reports.md +483 -0
  67. package/docs/retry.md +274 -0
  68. package/docs/secrets.md +150 -0
  69. package/docs/sessions.md +80 -0
  70. package/docs/shadow.md +68 -0
  71. package/docs/test-structure.md +275 -0
  72. package/docs/timeouts.md +183 -0
  73. package/docs/translation.md +247 -0
  74. package/docs/tutorial.md +271 -0
  75. package/docs/typescript.md +374 -0
  76. package/docs/web-element.md +251 -0
  77. package/docs/webdriver.md +708 -0
  78. package/docs/within.md +55 -0
  79. package/lib/ai.js +3 -2
  80. package/lib/aria.js +260 -0
  81. package/lib/assertions.js +18 -0
  82. package/lib/codecept.js +26 -23
  83. package/lib/command/check.js +2 -1
  84. package/lib/command/dryRun.js +24 -5
  85. package/lib/command/generate.js +2 -0
  86. package/lib/command/gherkin/snippets.js +5 -4
  87. package/lib/command/init.js +248 -269
  88. package/lib/command/list.js +150 -10
  89. package/lib/command/query.js +218 -0
  90. package/lib/command/run-multiple.js +2 -0
  91. package/lib/command/run-workers.js +2 -0
  92. package/lib/command/run.js +1 -1
  93. package/lib/command/workers/runTests.js +10 -10
  94. package/lib/config.js +77 -4
  95. package/lib/container.js +114 -17
  96. package/lib/effects.js +17 -0
  97. package/lib/element/WebElement.js +246 -2
  98. package/lib/els.js +12 -6
  99. package/lib/globals.js +32 -19
  100. package/lib/heal.js +4 -3
  101. package/lib/helper/ApiDataFactory.js +2 -1
  102. package/lib/helper/Appium.js +8 -8
  103. package/lib/helper/FileSystem.js +3 -2
  104. package/lib/helper/GraphQLDataFactory.js +2 -1
  105. package/lib/helper/Playwright.js +228 -162
  106. package/lib/helper/Puppeteer.js +208 -76
  107. package/lib/helper/WebDriver.js +173 -68
  108. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  109. package/lib/helper/errors/NonFocusedType.js +8 -0
  110. package/lib/helper/extras/Download.js +45 -0
  111. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  112. package/lib/helper/extras/elementSelection.js +58 -0
  113. package/lib/helper/extras/focusCheck.js +43 -0
  114. package/lib/helper/extras/richTextEditor.js +178 -0
  115. package/lib/helper/scripts/dropFile.js +11 -0
  116. package/lib/history.js +3 -2
  117. package/lib/html.js +103 -16
  118. package/lib/index.js +9 -1
  119. package/lib/listener/config.js +6 -4
  120. package/lib/listener/emptyRun.js +2 -1
  121. package/lib/listener/globalRetry.js +32 -6
  122. package/lib/listener/helpers.js +4 -1
  123. package/lib/listener/mocha.js +2 -1
  124. package/lib/listener/pageobjects.js +43 -0
  125. package/lib/listener/result.js +3 -2
  126. package/lib/locator.js +126 -3
  127. package/lib/mocha/cli.js +14 -2
  128. package/lib/mocha/factory.js +7 -2
  129. package/lib/mocha/inject.js +1 -1
  130. package/lib/mocha/scenarioConfig.js +2 -1
  131. package/lib/mocha/ui.js +5 -6
  132. package/lib/parser.js +2 -2
  133. package/lib/pause.js +38 -4
  134. package/lib/plugin/aiTrace.js +453 -0
  135. package/lib/plugin/analyze.js +1 -1
  136. package/lib/plugin/auth.js +3 -3
  137. package/lib/plugin/browser.js +77 -0
  138. package/lib/plugin/expose.js +159 -0
  139. package/lib/plugin/heal.js +44 -1
  140. package/lib/plugin/pageInfo.js +53 -49
  141. package/lib/plugin/pause.js +131 -0
  142. package/lib/plugin/pauseOnFail.js +10 -34
  143. package/lib/plugin/retryFailedStep.js +28 -19
  144. package/lib/plugin/screencast.js +287 -0
  145. package/lib/plugin/screenshot.js +563 -0
  146. package/lib/plugin/screenshotOnFail.js +8 -171
  147. package/lib/rerun.js +2 -1
  148. package/lib/result.js +2 -1
  149. package/lib/step/base.js +3 -2
  150. package/lib/step/config.js +15 -2
  151. package/lib/step/record.js +2 -2
  152. package/lib/store.js +72 -3
  153. package/lib/translation.js +2 -1
  154. package/lib/utils/mask_data.js +2 -1
  155. package/lib/utils/pluginParser.js +151 -0
  156. package/lib/utils/trace.js +297 -0
  157. package/lib/utils.js +77 -3
  158. package/lib/workers.js +52 -22
  159. package/package.json +19 -13
  160. package/typings/index.d.ts +19 -5
  161. package/docs/webapi/amOnPage.mustache +0 -11
  162. package/docs/webapi/appendField.mustache +0 -11
  163. package/docs/webapi/attachFile.mustache +0 -12
  164. package/docs/webapi/blur.mustache +0 -18
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -9
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -29
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  177. package/docs/webapi/dontSeeElement.mustache +0 -8
  178. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  179. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  180. package/docs/webapi/dontSeeInField.mustache +0 -11
  181. package/docs/webapi/dontSeeInSource.mustache +0 -8
  182. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  183. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  184. package/docs/webapi/doubleClick.mustache +0 -13
  185. package/docs/webapi/downloadFile.mustache +0 -12
  186. package/docs/webapi/dragAndDrop.mustache +0 -9
  187. package/docs/webapi/dragSlider.mustache +0 -11
  188. package/docs/webapi/executeAsyncScript.mustache +0 -24
  189. package/docs/webapi/executeScript.mustache +0 -26
  190. package/docs/webapi/fillField.mustache +0 -16
  191. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  192. package/docs/webapi/focus.mustache +0 -13
  193. package/docs/webapi/forceClick.mustache +0 -28
  194. package/docs/webapi/forceRightClick.mustache +0 -18
  195. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  196. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  197. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  198. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  199. package/docs/webapi/grabCookie.mustache +0 -11
  200. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  201. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  202. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  203. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  204. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  205. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  206. package/docs/webapi/grabGeoLocation.mustache +0 -8
  207. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  208. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  209. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  210. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  211. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  212. package/docs/webapi/grabPopupText.mustache +0 -5
  213. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  214. package/docs/webapi/grabSource.mustache +0 -8
  215. package/docs/webapi/grabTextFrom.mustache +0 -10
  216. package/docs/webapi/grabTextFromAll.mustache +0 -9
  217. package/docs/webapi/grabTitle.mustache +0 -8
  218. package/docs/webapi/grabValueFrom.mustache +0 -9
  219. package/docs/webapi/grabValueFromAll.mustache +0 -8
  220. package/docs/webapi/grabWebElement.mustache +0 -9
  221. package/docs/webapi/grabWebElements.mustache +0 -9
  222. package/docs/webapi/moveCursorTo.mustache +0 -12
  223. package/docs/webapi/openNewTab.mustache +0 -7
  224. package/docs/webapi/pressKey.mustache +0 -12
  225. package/docs/webapi/pressKeyDown.mustache +0 -12
  226. package/docs/webapi/pressKeyUp.mustache +0 -12
  227. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  228. package/docs/webapi/refreshPage.mustache +0 -6
  229. package/docs/webapi/resizeWindow.mustache +0 -6
  230. package/docs/webapi/rightClick.mustache +0 -14
  231. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  232. package/docs/webapi/saveScreenshot.mustache +0 -12
  233. package/docs/webapi/say.mustache +0 -10
  234. package/docs/webapi/scrollIntoView.mustache +0 -11
  235. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  236. package/docs/webapi/scrollPageToTop.mustache +0 -6
  237. package/docs/webapi/scrollTo.mustache +0 -12
  238. package/docs/webapi/see.mustache +0 -11
  239. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  240. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  241. package/docs/webapi/seeCookie.mustache +0 -8
  242. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  243. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  244. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  245. package/docs/webapi/seeElement.mustache +0 -8
  246. package/docs/webapi/seeElementInDOM.mustache +0 -8
  247. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  248. package/docs/webapi/seeInField.mustache +0 -12
  249. package/docs/webapi/seeInPopup.mustache +0 -8
  250. package/docs/webapi/seeInSource.mustache +0 -7
  251. package/docs/webapi/seeInTitle.mustache +0 -8
  252. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  253. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  254. package/docs/webapi/seeTextEquals.mustache +0 -9
  255. package/docs/webapi/seeTitleEquals.mustache +0 -8
  256. package/docs/webapi/seeTraffic.mustache +0 -36
  257. package/docs/webapi/selectOption.mustache +0 -21
  258. package/docs/webapi/setCookie.mustache +0 -16
  259. package/docs/webapi/setGeoLocation.mustache +0 -12
  260. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  261. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  262. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  263. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  264. package/docs/webapi/switchTo.mustache +0 -9
  265. package/docs/webapi/switchToNextTab.mustache +0 -10
  266. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  267. package/docs/webapi/type.mustache +0 -21
  268. package/docs/webapi/uncheckOption.mustache +0 -13
  269. package/docs/webapi/wait.mustache +0 -8
  270. package/docs/webapi/waitForClickable.mustache +0 -11
  271. package/docs/webapi/waitForCookie.mustache +0 -9
  272. package/docs/webapi/waitForDetached.mustache +0 -10
  273. package/docs/webapi/waitForDisabled.mustache +0 -6
  274. package/docs/webapi/waitForElement.mustache +0 -11
  275. package/docs/webapi/waitForEnabled.mustache +0 -6
  276. package/docs/webapi/waitForFunction.mustache +0 -17
  277. package/docs/webapi/waitForInvisible.mustache +0 -10
  278. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  279. package/docs/webapi/waitForText.mustache +0 -13
  280. package/docs/webapi/waitForValue.mustache +0 -10
  281. package/docs/webapi/waitForVisible.mustache +0 -10
  282. package/docs/webapi/waitInUrl.mustache +0 -9
  283. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  284. package/docs/webapi/waitToHide.mustache +0 -10
  285. package/docs/webapi/waitUrlEquals.mustache +0 -10
  286. package/lib/helper/AI.js +0 -214
  287. package/lib/listener/enhancedGlobalRetry.js +0 -110
  288. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  289. package/lib/plugin/htmlReporter.js +0 -3648
  290. package/lib/plugin/stepByStepReport.js +0 -427
  291. package/lib/plugin/subtitles.js +0 -89
  292. package/lib/retryCoordinator.js +0 -207
  293. package/typings/promiseBasedTypes.d.ts +0 -9469
  294. 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
@@ -1490,8 +1494,23 @@ class Playwright extends Helper {
1490
1494
  *
1491
1495
  */
1492
1496
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1493
- const el = await this._locateElement(locator)
1494
- assertElementExists(el, locator)
1497
+ let context = null
1498
+ if (typeof offsetX !== 'number') {
1499
+ context = offsetX
1500
+ offsetX = 0
1501
+ }
1502
+
1503
+ let el
1504
+ if (context) {
1505
+ const contextEls = await this._locate(context)
1506
+ assertElementExists(contextEls, context, 'Context element')
1507
+ el = await findElements.call(this, contextEls[0], locator)
1508
+ assertElementExists(el, locator)
1509
+ el = el[0]
1510
+ } else {
1511
+ el = await this._locateElement(locator)
1512
+ assertElementExists(el, locator)
1513
+ }
1495
1514
 
1496
1515
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
1497
1516
  const { x, y } = await clickablePoint(el)
@@ -1615,7 +1634,7 @@ class Playwright extends Helper {
1615
1634
  * @returns Promise<void>
1616
1635
  */
1617
1636
  async replayFromHar(harFilePath, opts) {
1618
- const file = path.join(global.codecept_dir, harFilePath)
1637
+ const file = path.join(store.codeceptDir, harFilePath)
1619
1638
 
1620
1639
  if (!fileExists(file)) {
1621
1640
  throw new Error(`File at ${file} cannot be found on local system`)
@@ -1759,8 +1778,7 @@ class Playwright extends Helper {
1759
1778
  if (elements.length === 0) {
1760
1779
  throw new ElementNotFound(locator, 'Element', 'was not found')
1761
1780
  }
1762
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1763
- return elements[0]
1781
+ return selectElement(elements, locator, this)
1764
1782
  }
1765
1783
 
1766
1784
  /**
@@ -1775,8 +1793,7 @@ class Playwright extends Helper {
1775
1793
  const context = providedContext || (await this._getContext())
1776
1794
  const els = await findCheckable.call(this, locator, context)
1777
1795
  assertElementExists(els[0], locator, 'Checkbox or radio')
1778
- if (this.options.strict) assertOnlyOneElement(els, locator)
1779
- return els[0]
1796
+ return selectElement(els, locator, this)
1780
1797
  }
1781
1798
 
1782
1799
  /**
@@ -1944,8 +1961,15 @@ class Playwright extends Helper {
1944
1961
  * {{> seeElement }}
1945
1962
  *
1946
1963
  */
1947
- async seeElement(locator) {
1948
- let els = await this._locate(locator)
1964
+ async seeElement(locator, context = null) {
1965
+ let els
1966
+ if (context) {
1967
+ const contextEls = await this._locate(context)
1968
+ assertElementExists(contextEls, context, 'Context element')
1969
+ els = await findElements.call(this, contextEls[0], locator)
1970
+ } else {
1971
+ els = await this._locate(locator)
1972
+ }
1949
1973
  els = await Promise.all(els.map(el => el.isVisible()))
1950
1974
  try {
1951
1975
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
@@ -1958,8 +1982,15 @@ class Playwright extends Helper {
1958
1982
  * {{> dontSeeElement }}
1959
1983
  *
1960
1984
  */
1961
- async dontSeeElement(locator) {
1962
- let els = await this._locate(locator)
1985
+ async dontSeeElement(locator, context = null) {
1986
+ let els
1987
+ if (context) {
1988
+ const contextEls = await this._locate(context)
1989
+ assertElementExists(contextEls, context, 'Context element')
1990
+ els = await findElements.call(this, contextEls[0], locator)
1991
+ } else {
1992
+ els = await this._locate(locator)
1993
+ }
1963
1994
  els = await Promise.all(els.map(el => el.isVisible()))
1964
1995
  try {
1965
1996
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
@@ -2014,7 +2045,7 @@ class Playwright extends Helper {
2014
2045
  const filePath = await download.path()
2015
2046
  fileName = fileName || `downloads/${path.basename(filePath)}`
2016
2047
 
2017
- const downloadPath = path.join(global.output_dir, fileName)
2048
+ const downloadPath = path.join(store.outputDir, fileName)
2018
2049
  if (!fs.existsSync(path.dirname(downloadPath))) {
2019
2050
  fs.mkdirSync(path.dirname(downloadPath), '0777')
2020
2051
  }
@@ -2198,6 +2229,7 @@ class Playwright extends Helper {
2198
2229
  * {{> pressKeyWithKeyNormalization }}
2199
2230
  */
2200
2231
  async pressKey(key) {
2232
+ await checkFocusBeforePressKey(this, key)
2201
2233
  const modifiers = []
2202
2234
  if (Array.isArray(key)) {
2203
2235
  for (let k of key) {
@@ -2226,6 +2258,8 @@ class Playwright extends Helper {
2226
2258
  * {{> type }}
2227
2259
  */
2228
2260
  async type(keys, delay = null) {
2261
+ await checkFocusBeforeType(this)
2262
+
2229
2263
  // Always use page.keyboard.type for any string (including single character and national characters).
2230
2264
  if (!Array.isArray(keys)) {
2231
2265
  keys = keys.toString()
@@ -2245,45 +2279,33 @@ class Playwright extends Helper {
2245
2279
  * {{> fillField }}
2246
2280
  *
2247
2281
  */
2248
- async fillField(field, value) {
2249
- const els = await findFields.call(this, field)
2282
+ async fillField(field, value, context = null) {
2283
+ const els = await findFields.call(this, field, context)
2250
2284
  assertElementExists(els, field, 'Field')
2251
- if (this.options.strict) assertOnlyOneElement(els, field)
2252
- const el = els[0]
2285
+ const el = selectElement(els, field, this)
2286
+
2287
+ await highlightActiveElement.call(this, el)
2288
+
2289
+ if (await fillRichEditor(this, el, value)) {
2290
+ return this._waitForAction()
2291
+ }
2253
2292
 
2254
2293
  await el.clear()
2255
2294
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
2256
2295
 
2257
- await highlightActiveElement.call(this, el)
2258
-
2259
2296
  await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2260
2297
 
2261
2298
  return this._waitForAction()
2262
2299
  }
2263
2300
 
2264
2301
  /**
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.
2302
+ * {{> clearField }}
2280
2303
  */
2281
- async clearField(locator, options = {}) {
2282
- const els = await findFields.call(this, locator)
2304
+ async clearField(locator, context = null) {
2305
+ const els = await findFields.call(this, locator, context)
2283
2306
  assertElementExists(els, locator, 'Field to clear')
2284
- if (this.options.strict) assertOnlyOneElement(els, locator)
2285
2307
 
2286
- const el = els[0]
2308
+ const el = selectElement(els, locator, this)
2287
2309
 
2288
2310
  await highlightActiveElement.call(this, el)
2289
2311
 
@@ -2295,76 +2317,101 @@ class Playwright extends Helper {
2295
2317
  /**
2296
2318
  * {{> appendField }}
2297
2319
  */
2298
- async appendField(field, value) {
2299
- const els = await findFields.call(this, field)
2320
+ async appendField(field, value, context = null) {
2321
+ const els = await findFields.call(this, field, context)
2300
2322
  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 })
2323
+ const el = selectElement(els, field, this)
2324
+ await highlightActiveElement.call(this, el)
2325
+ await el.press('End')
2326
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2305
2327
  return this._waitForAction()
2306
2328
  }
2307
2329
 
2308
2330
  /**
2309
2331
  * {{> seeInField }}
2310
2332
  */
2311
- async seeInField(field, value) {
2333
+ async seeInField(field, value, context = null) {
2312
2334
  const _value = typeof value === 'boolean' ? value : value.toString()
2313
- return proceedSeeInField.call(this, 'assert', field, _value)
2335
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
2314
2336
  }
2315
2337
 
2316
2338
  /**
2317
2339
  * {{> dontSeeInField }}
2318
2340
  */
2319
- async dontSeeInField(field, value) {
2341
+ async dontSeeInField(field, value, context = null) {
2320
2342
  const _value = typeof value === 'boolean' ? value : value.toString()
2321
- return proceedSeeInField.call(this, 'negate', field, _value)
2343
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
2322
2344
  }
2323
2345
 
2324
2346
  /**
2325
2347
  * {{> attachFile }}
2326
2348
  *
2327
2349
  */
2328
- async attachFile(locator, pathToFile) {
2329
- const file = path.join(global.codecept_dir, pathToFile)
2350
+ async attachFile(locator, pathToFile, context = null) {
2351
+ const file = path.join(store.codeceptDir, pathToFile)
2330
2352
 
2331
2353
  if (!fileExists(file)) {
2332
2354
  throw new Error(`File at ${file} can not be found on local system`)
2333
2355
  }
2334
- const els = await findFields.call(this, locator)
2335
- assertElementExists(els, locator, 'Field')
2336
- await els[0].setInputFiles(file)
2356
+ const els = await findFields.call(this, locator, context)
2357
+ if (els.length) {
2358
+ const el = selectElement(els, locator, this)
2359
+ const tag = await el.evaluate(el => el.tagName)
2360
+ const type = await el.evaluate(el => el.type)
2361
+ if (tag === 'INPUT' && type === 'file') {
2362
+ await el.setInputFiles(file)
2363
+ return this._waitForAction()
2364
+ }
2365
+ }
2366
+
2367
+ const targetEls = els.length ? els : await this._locate(locator)
2368
+ assertElementExists(targetEls, locator, 'Element')
2369
+ const el = selectElement(targetEls, locator, this)
2370
+ const fileData = {
2371
+ base64Content: base64EncodeFile(file),
2372
+ fileName: path.basename(file),
2373
+ mimeType: getMimeType(path.basename(file)),
2374
+ }
2375
+ await el.evaluate(dropFile, fileData)
2337
2376
  return this._waitForAction()
2338
2377
  }
2339
2378
 
2340
2379
  /**
2341
2380
  * {{> selectOption }}
2342
2381
  */
2343
- async selectOption(select, option) {
2344
- const context = await this.context
2382
+ async selectOption(select, option, context = null) {
2383
+ const pageContext = await this.context
2345
2384
  const matchedLocator = new Locator(select)
2346
2385
 
2386
+ let contextEl
2387
+ if (context) {
2388
+ const contextEls = await this._locate(context)
2389
+ assertElementExists(contextEls, context, 'Context element')
2390
+ contextEl = contextEls[0]
2391
+ }
2392
+
2347
2393
  // Strict locator
2348
2394
  if (!matchedLocator.isFuzzy()) {
2349
2395
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2350
- const els = await this._locate(matchedLocator)
2396
+ const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2351
2397
  assertElementExists(els, select, 'Selectable element')
2352
- return proceedSelect.call(this, context, els[0], option)
2398
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2353
2399
  }
2354
2400
 
2355
2401
  // Fuzzy: try combobox
2356
2402
  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)
2403
+ const comboboxSearchCtx = contextEl || pageContext
2404
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2405
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2359
2406
 
2360
2407
  // 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)
2408
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2409
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2363
2410
 
2364
2411
  // Fuzzy: try native select
2365
- els = await findFields.call(this, select)
2412
+ els = await findFields.call(this, select, context)
2366
2413
  assertElementExists(els, select, 'Selectable element')
2367
- return proceedSelect.call(this, context, els[0], option)
2414
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2368
2415
  }
2369
2416
 
2370
2417
  /**
@@ -2412,7 +2459,7 @@ class Playwright extends Helper {
2412
2459
  const currentUrl = await this._getPageUrl()
2413
2460
  const baseUrl = this.options.url || 'http://localhost'
2414
2461
  const actualPath = new URL(currentUrl, baseUrl).pathname
2415
- return equals('url path').assert(path, actualPath)
2462
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
2416
2463
  }
2417
2464
 
2418
2465
  /**
@@ -2422,7 +2469,7 @@ class Playwright extends Helper {
2422
2469
  const currentUrl = await this._getPageUrl()
2423
2470
  const baseUrl = this.options.url || 'http://localhost'
2424
2471
  const actualPath = new URL(currentUrl, baseUrl).pathname
2425
- return equals('url path').negate(path, actualPath)
2472
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
2426
2473
  }
2427
2474
 
2428
2475
  /**
@@ -2627,8 +2674,11 @@ class Playwright extends Helper {
2627
2674
  * @returns {Promise<any>}
2628
2675
  */
2629
2676
  async executeScript(fn, arg) {
2677
+ if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
2678
+ if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
2679
+ return arg.evaluate(fn)
2680
+ }
2630
2681
  if (this.context && this.context.constructor.name === 'FrameLocator') {
2631
- // switching to iframe context
2632
2682
  return this.context.locator(':root').evaluate(fn, arg)
2633
2683
  }
2634
2684
  return this.page.evaluate.apply(this.page, [fn, arg])
@@ -2658,15 +2708,12 @@ class Playwright extends Helper {
2658
2708
  *
2659
2709
  */
2660
2710
  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
- }
2711
+ const roleElements = await handleRoleLocator(this.page, locator)
2712
+ if (roleElements && roleElements.length > 0) {
2713
+ const text = await roleElements[0].textContent()
2714
+ assertElementExists(text, JSON.stringify(locator))
2715
+ this.debugSection('Text', text)
2716
+ return text
2670
2717
  }
2671
2718
 
2672
2719
  const locatorObj = new Locator(locator, 'css')
@@ -2894,7 +2941,7 @@ class Playwright extends Helper {
2894
2941
  const els = await this._locate(matchedLocator)
2895
2942
  assertElementExists(els, locator)
2896
2943
  const snapshot = await els[0].ariaSnapshot()
2897
- this.debugSection('Aria Snapshot', snapshot)
2944
+ this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
2898
2945
  return snapshot
2899
2946
  }
2900
2947
 
@@ -3382,6 +3429,7 @@ class Playwright extends Helper {
3382
3429
  */
3383
3430
  async waitInUrl(urlPart, sec = null) {
3384
3431
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3432
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3385
3433
 
3386
3434
  return this.page
3387
3435
  .waitForFunction(
@@ -3389,13 +3437,13 @@ class Playwright extends Helper {
3389
3437
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
3390
3438
  return currUrl.indexOf(urlPart) > -1
3391
3439
  },
3392
- urlPart,
3440
+ expectedUrl,
3393
3441
  { timeout: waitTimeout },
3394
3442
  )
3395
3443
  .catch(async e => {
3396
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
3444
+ const currUrl = await this._getPageUrl()
3397
3445
  if (/Timeout/i.test(e.message)) {
3398
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
3446
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
3399
3447
  } else {
3400
3448
  throw e
3401
3449
  }
@@ -3407,26 +3455,46 @@ class Playwright extends Helper {
3407
3455
  */
3408
3456
  async waitUrlEquals(urlPart, sec = null) {
3409
3457
  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
- }
3458
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3416
3459
 
3417
3460
  try {
3418
3461
  await this.page.waitForURL(
3419
- url => url.href.includes(expectedUrl),
3462
+ url => url.href === expectedUrl,
3420
3463
  { timeout: waitTimeout },
3421
3464
  )
3422
3465
  } catch (e) {
3423
3466
  const currUrl = await this._getPageUrl()
3424
3467
  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
- }
3468
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3469
+ } else {
3470
+ throw e
3471
+ }
3472
+ }
3473
+ }
3474
+
3475
+ /**
3476
+ * {{> waitCurrentPathEquals }}
3477
+ */
3478
+ async waitCurrentPathEquals(path, sec = null) {
3479
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3480
+ const normalizedPath = normalizePath(path)
3481
+
3482
+ try {
3483
+ await this.page.waitForFunction(
3484
+ expectedPath => {
3485
+ const actualPath = window.location.pathname
3486
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
3487
+ return normalizePath(actualPath) === expectedPath
3488
+ },
3489
+ normalizedPath,
3490
+ { timeout: waitTimeout },
3491
+ )
3492
+ } catch (e) {
3493
+ const currentUrl = await this._getPageUrl()
3494
+ const baseUrl = this.options.url || 'http://localhost'
3495
+ const actualPath = new URL(currentUrl, baseUrl).pathname
3496
+ if (/Timeout/i.test(e.message)) {
3497
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
3430
3498
  } else {
3431
3499
  throw e
3432
3500
  }
@@ -4112,9 +4180,15 @@ class Playwright extends Helper {
4112
4180
 
4113
4181
  export default Playwright
4114
4182
 
4115
- function buildLocatorString(locator) {
4183
+ export function buildLocatorString(locator) {
4116
4184
  if (locator.isXPath()) {
4117
- return `xpath=${locator.value}`
4185
+ // Make XPath relative so it works correctly within scoped contexts (e.g. within()).
4186
+ // Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
4187
+ // but only when the selector starts with "/". Locator methods like at() wrap XPath in
4188
+ // parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
4189
+ // We fix this by prepending "." before the first "//" that follows any leading parentheses.
4190
+ const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
4191
+ return `xpath=${value}`
4118
4192
  }
4119
4193
  if (locator.isShadow()) {
4120
4194
  // Convert shadow locator to CSS with >> chaining operator
@@ -4125,25 +4199,22 @@ function buildLocatorString(locator) {
4125
4199
  return locator.simplify()
4126
4200
  }
4127
4201
 
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
4202
  /**
4136
4203
  * Handles role locator objects by converting them to Playwright's getByRole() API
4204
+ * Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
4137
4205
  * Returns elements array if role locator, null otherwise
4138
4206
  */
4139
4207
  async function handleRoleLocator(context, locator) {
4140
- if (!isRoleLocatorObject(locator)) return null
4208
+ const loc = new Locator(locator)
4209
+ if (!loc.isRole()) return null
4141
4210
 
4211
+ const roleObj = loc.locator || {}
4142
4212
  const options = {}
4143
- if (locator.text) options.name = locator.text
4144
- if (locator.exact !== undefined) options.exact = locator.exact
4213
+ if (roleObj.text) options.name = roleObj.text
4214
+ if (roleObj.name) options.name = roleObj.name
4215
+ if (roleObj.exact !== undefined) options.exact = roleObj.exact
4145
4216
 
4146
- return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4217
+ return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
4147
4218
  }
4148
4219
 
4149
4220
  async function findByRole(context, locator) {
@@ -4155,13 +4226,10 @@ async function findByRole(context, locator) {
4155
4226
  }
4156
4227
 
4157
4228
  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
4229
  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
4230
  const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4162
4231
 
4163
4232
  if (isReactLocator) return findReact(matcher, locator)
4164
- if (isVueLocator) return findVue(matcher, locator)
4165
4233
  if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4166
4234
 
4167
4235
  // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
@@ -4177,7 +4245,6 @@ async function findElements(matcher, locator) {
4177
4245
 
4178
4246
  async function findElement(matcher, locator) {
4179
4247
  if (locator.react) return findReact(matcher, locator)
4180
- if (locator.vue) return findVue(matcher, locator)
4181
4248
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4182
4249
 
4183
4250
  locator = new Locator(locator, 'css')
@@ -4212,16 +4279,22 @@ async function proceedClick(locator, context = null, options = {}) {
4212
4279
  assertElementExists(els, locator, 'Clickable element')
4213
4280
  }
4214
4281
 
4215
- await highlightActiveElement.call(this, els[0])
4216
- if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
4282
+ const opts = store.currentStep?.opts
4283
+ let element
4284
+ if (opts?.elementIndex != null) {
4285
+ element = selectElement(els, locator, this)
4286
+ } else {
4287
+ const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
4288
+ if (strict) assertOnlyOneElement(els, locator, this)
4289
+ element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4290
+ }
4291
+
4292
+ await highlightActiveElement.call(this, element)
4293
+ if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
4217
4294
 
4218
- /*
4219
- using the force true options itself but instead dispatching a click
4220
- */
4221
4295
  if (options.force) {
4222
- await els[0].dispatchEvent('click')
4296
+ await element.dispatchEvent('click')
4223
4297
  } else {
4224
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4225
4298
  await element.click(options)
4226
4299
  }
4227
4300
  const promises = []
@@ -4238,7 +4311,6 @@ async function findClickable(matcher, locator) {
4238
4311
 
4239
4312
  if (!matchedLocator.isFuzzy()) {
4240
4313
  const els = await findElements.call(this, matcher, matchedLocator)
4241
- if (this.options.strict) assertOnlyOneElement(els, locator)
4242
4314
  return els
4243
4315
  }
4244
4316
 
@@ -4247,42 +4319,27 @@ async function findClickable(matcher, locator) {
4247
4319
 
4248
4320
  try {
4249
4321
  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
- }
4322
+ if (els.length) return els
4254
4323
  } catch (err) {
4255
4324
  // getByRole not supported or failed
4256
4325
  }
4257
4326
 
4258
4327
  try {
4259
4328
  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
- }
4329
+ if (els.length) return els
4264
4330
  } catch (err) {
4265
4331
  // getByRole not supported or failed
4266
4332
  }
4267
4333
 
4268
4334
  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
- }
4335
+ if (els.length) return els
4273
4336
 
4274
4337
  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
- }
4338
+ if (els.length) return els
4279
4339
 
4280
4340
  try {
4281
4341
  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
- }
4342
+ if (els.length) return els
4286
4343
  } catch (err) {
4287
4344
  // Do nothing
4288
4345
  }
@@ -4355,34 +4412,42 @@ async function proceedIsChecked(assertType, option) {
4355
4412
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
4356
4413
  }
4357
4414
 
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
4415
+ async function findFields(locator, context = null) {
4416
+ let contextEl
4417
+ if (context) {
4418
+ const contextEls = await this._locate(context)
4419
+ assertElementExists(contextEls, context, 'Context element')
4420
+ contextEl = contextEls[0]
4364
4421
  }
4365
4422
 
4423
+ const locateFn = contextEl
4424
+ ? loc => findElements.call(this, contextEl, loc)
4425
+ : loc => this._locate(loc)
4426
+
4427
+ const matcher = contextEl || (await this.page)
4428
+ const roleElements = await handleRoleLocator(matcher, locator)
4429
+ if (roleElements) return roleElements
4430
+
4366
4431
  const matchedLocator = new Locator(locator)
4367
4432
  if (!matchedLocator.isFuzzy()) {
4368
- return this._locate(matchedLocator)
4433
+ return locateFn(matchedLocator)
4369
4434
  }
4370
4435
  const literal = xpathLocator.literal(locator)
4371
4436
 
4372
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
4437
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
4373
4438
  if (els.length) {
4374
4439
  return els
4375
4440
  }
4376
4441
 
4377
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
4442
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
4378
4443
  if (els.length) {
4379
4444
  return els
4380
4445
  }
4381
- els = await this._locate({ xpath: Locator.field.byName(literal) })
4446
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
4382
4447
  if (els.length) {
4383
4448
  return els
4384
4449
  }
4385
- return this._locate({ css: locator })
4450
+ return locateFn({ css: locator })
4386
4451
  }
4387
4452
 
4388
4453
  async function proceedSelect(context, el, option) {
@@ -4431,8 +4496,8 @@ async function proceedSelect(context, el, option) {
4431
4496
  return this._waitForAction()
4432
4497
  }
4433
4498
 
4434
- async function proceedSeeInField(assertType, field, value) {
4435
- const els = await findFields.call(this, field)
4499
+ async function proceedSeeInField(assertType, field, value, context) {
4500
+ const els = await findFields.call(this, field, context)
4436
4501
  assertElementExists(els, field, 'Field')
4437
4502
  const el = els[0]
4438
4503
  const tag = await el.evaluate(e => e.tagName)
@@ -4546,9 +4611,10 @@ function assertElementExists(res, locator, prefix, suffix) {
4546
4611
  }
4547
4612
  }
4548
4613
 
4549
- function assertOnlyOneElement(elements, locator) {
4614
+ function assertOnlyOneElement(elements, locator, helper) {
4550
4615
  if (elements.length > 1) {
4551
- throw new MultipleElementsFound(locator, elements)
4616
+ const webElements = elements.map(el => new WebElement(el, helper))
4617
+ throw new MultipleElementsFound(locator, webElements)
4552
4618
  }
4553
4619
  }
4554
4620
 
@@ -4757,7 +4823,7 @@ async function refreshContextSession() {
4757
4823
 
4758
4824
  function saveVideoForPage(page, name) {
4759
4825
  if (!page.video()) return null
4760
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4826
+ const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4761
4827
  page
4762
4828
  .video()
4763
4829
  .saveAs(fileName)
@@ -4774,7 +4840,7 @@ async function saveTraceForContext(context, name) {
4774
4840
  if (!context) return
4775
4841
  if (!context.tracing) return
4776
4842
  try {
4777
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4843
+ const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4778
4844
  await context.tracing.stop({ path: fileName })
4779
4845
  return fileName
4780
4846
  } catch (err) {