codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10

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 (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. package/typings/types.d.ts +0 -13073
@@ -26,17 +26,24 @@ import {
26
26
  isModifierKey,
27
27
  requireWithFallback,
28
28
  normalizeSpacesInString,
29
+ normalizePath,
30
+ resolveUrl,
31
+ getMimeType,
32
+ base64EncodeFile,
29
33
  } from '../utils.js'
30
34
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
35
  import ElementNotFound from './errors/ElementNotFound.js'
36
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
32
37
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
33
38
  import Popup from './extras/Popup.js'
34
39
  import Console from './extras/Console.js'
35
40
  import { highlightElement } from './scripts/highlightElement.js'
36
41
  import { blurElement } from './scripts/blurElement.js'
42
+ import { dropFile } from './scripts/dropFile.js'
37
43
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
44
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
39
45
  import WebElement from '../element/WebElement.js'
46
+ import { selectElement } from './extras/elementSelection.js'
40
47
 
41
48
  let puppeteer
42
49
 
@@ -266,6 +273,7 @@ class Puppeteer extends Helper {
266
273
  show: false,
267
274
  defaultPopupAction: 'accept',
268
275
  highlightElement: false,
276
+ strict: false,
269
277
  }
270
278
 
271
279
  return Object.assign(defaults, config)
@@ -814,9 +822,26 @@ class Puppeteer extends Helper {
814
822
  * {{ react }}
815
823
  */
816
824
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
817
- const el = await this._locateElement(locator)
818
- if (!el) {
819
- throw new ElementNotFound(locator, 'Element to move cursor to')
825
+ let context = null
826
+ if (typeof offsetX !== 'number') {
827
+ context = offsetX
828
+ offsetX = 0
829
+ }
830
+
831
+ let el
832
+ if (context) {
833
+ const contextEls = await findElements.call(this, this.page, context)
834
+ assertElementExists(contextEls, context, 'Context element')
835
+ const els = await findElements.call(this, contextEls[0], locator)
836
+ if (!els || els.length === 0) {
837
+ throw new ElementNotFound(locator, 'Element to move cursor to')
838
+ }
839
+ el = els[0]
840
+ } else {
841
+ el = await this._locateElement(locator)
842
+ if (!el) {
843
+ throw new ElementNotFound(locator, 'Element to move cursor to')
844
+ }
820
845
  }
821
846
 
822
847
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -984,6 +1009,14 @@ class Puppeteer extends Helper {
984
1009
  */
985
1010
  async _locateElement(locator) {
986
1011
  const context = await this.context
1012
+ const elementIndex = store.currentStep?.opts?.elementIndex
1013
+ if (this.options.strict || elementIndex) {
1014
+ const elements = await findElements.call(this, context, locator)
1015
+ if (elements.length === 0) {
1016
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1017
+ }
1018
+ return selectElement(elements, locator, this)
1019
+ }
987
1020
  return findElement.call(this, context, locator)
988
1021
  }
989
1022
 
@@ -1001,7 +1034,7 @@ class Puppeteer extends Helper {
1001
1034
  if (!els || els.length === 0) {
1002
1035
  throw new ElementNotFound(locator, 'Checkbox or radio')
1003
1036
  }
1004
- return els[0]
1037
+ return selectElement(els, locator, this)
1005
1038
  }
1006
1039
 
1007
1040
  /**
@@ -1158,8 +1191,16 @@ class Puppeteer extends Helper {
1158
1191
  * {{> seeElement }}
1159
1192
  * {{ react }}
1160
1193
  */
1161
- async seeElement(locator) {
1162
- let els = await this._locate(locator)
1194
+ async seeElement(locator, context = null) {
1195
+ let els
1196
+ if (context) {
1197
+ const contextPage = await this.context
1198
+ const contextEls = await findElements.call(this, contextPage, context)
1199
+ assertElementExists(contextEls, context, 'Context element')
1200
+ els = await findElements.call(this, contextEls[0], locator)
1201
+ } else {
1202
+ els = await this._locate(locator)
1203
+ }
1163
1204
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1164
1205
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1165
1206
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1174,8 +1215,16 @@ class Puppeteer extends Helper {
1174
1215
  * {{> dontSeeElement }}
1175
1216
  * {{ react }}
1176
1217
  */
1177
- async dontSeeElement(locator) {
1178
- let els = await this._locate(locator)
1218
+ async dontSeeElement(locator, context = null) {
1219
+ let els
1220
+ if (context) {
1221
+ const contextPage = await this.context
1222
+ const contextEls = await findElements.call(this, contextPage, context)
1223
+ assertElementExists(contextEls, context, 'Context element')
1224
+ els = await findElements.call(this, contextEls[0], locator)
1225
+ } else {
1226
+ els = await this._locate(locator)
1227
+ }
1179
1228
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1180
1229
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1181
1230
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1541,10 +1590,10 @@ class Puppeteer extends Helper {
1541
1590
  * {{> fillField }}
1542
1591
  * {{ react }}
1543
1592
  */
1544
- async fillField(field, value) {
1545
- const els = await findVisibleFields.call(this, field)
1593
+ async fillField(field, value, context = null) {
1594
+ const els = await findVisibleFields.call(this, field, context)
1546
1595
  assertElementExists(els, field, 'Field')
1547
- const el = els[0]
1596
+ const el = selectElement(els, field, this)
1548
1597
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1549
1598
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1550
1599
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1562,8 +1611,8 @@ class Puppeteer extends Helper {
1562
1611
  /**
1563
1612
  * {{> clearField }}
1564
1613
  */
1565
- async clearField(field) {
1566
- return this.fillField(field, '')
1614
+ async clearField(field, context = null) {
1615
+ return this.fillField(field, '', context)
1567
1616
  }
1568
1617
 
1569
1618
  /**
@@ -1571,29 +1620,30 @@ class Puppeteer extends Helper {
1571
1620
  *
1572
1621
  * {{ react }}
1573
1622
  */
1574
- async appendField(field, value) {
1575
- const els = await findVisibleFields.call(this, field)
1623
+ async appendField(field, value, context = null) {
1624
+ const els = await findVisibleFields.call(this, field, context)
1576
1625
  assertElementExists(els, field, 'Field')
1577
- highlightActiveElement.call(this, els[0], await this._getContext())
1578
- await els[0].press('End')
1579
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
1626
+ const el = selectElement(els, field, this)
1627
+ highlightActiveElement.call(this, el, await this._getContext())
1628
+ await el.press('End')
1629
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1580
1630
  return this._waitForAction()
1581
1631
  }
1582
1632
 
1583
1633
  /**
1584
1634
  * {{> seeInField }}
1585
1635
  */
1586
- async seeInField(field, value) {
1636
+ async seeInField(field, value, context = null) {
1587
1637
  const _value = typeof value === 'boolean' ? value : value.toString()
1588
- return proceedSeeInField.call(this, 'assert', field, _value)
1638
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
1589
1639
  }
1590
1640
 
1591
1641
  /**
1592
1642
  * {{> dontSeeInField }}
1593
1643
  */
1594
- async dontSeeInField(field, value) {
1644
+ async dontSeeInField(field, value, context = null) {
1595
1645
  const _value = typeof value === 'boolean' ? value : value.toString()
1596
- return proceedSeeInField.call(this, 'negate', field, _value)
1646
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
1597
1647
  }
1598
1648
 
1599
1649
  /**
@@ -1601,49 +1651,71 @@ class Puppeteer extends Helper {
1601
1651
  *
1602
1652
  * {{> attachFile }}
1603
1653
  */
1604
- async attachFile(locator, pathToFile) {
1654
+ async attachFile(locator, pathToFile, context = null) {
1605
1655
  const file = path.join(global.codecept_dir, pathToFile)
1606
1656
 
1607
1657
  if (!fileExists(file)) {
1608
1658
  throw new Error(`File at ${file} can not be found on local system`)
1609
1659
  }
1610
- const els = await findFields.call(this, locator)
1611
- assertElementExists(els, locator, 'Field')
1612
- await els[0].uploadFile(file)
1660
+ const els = await findFields.call(this, locator, context)
1661
+ if (els.length) {
1662
+ const el = selectElement(els, locator, this)
1663
+ const tag = await el.evaluate(el => el.tagName)
1664
+ const type = await el.evaluate(el => el.type)
1665
+ if (tag === 'INPUT' && type === 'file') {
1666
+ await el.uploadFile(file)
1667
+ return this._waitForAction()
1668
+ }
1669
+ }
1670
+
1671
+ const targetEls = els.length ? els : await this._locate(locator)
1672
+ assertElementExists(targetEls, locator, 'Element')
1673
+ const el = selectElement(targetEls, locator, this)
1674
+ const fileData = {
1675
+ base64Content: base64EncodeFile(file),
1676
+ fileName: path.basename(file),
1677
+ mimeType: getMimeType(path.basename(file)),
1678
+ }
1679
+ await el.evaluate(dropFile, fileData)
1613
1680
  return this._waitForAction()
1614
1681
  }
1615
1682
 
1616
1683
  /**
1617
1684
  * {{> selectOption }}
1618
1685
  */
1619
- async selectOption(select, option) {
1620
- const els = await findVisibleFields.call(this, select)
1621
- assertElementExists(els, select, 'Selectable field')
1622
- const el = els[0]
1623
- if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
1624
- throw new Error('Element is not <select>')
1625
- }
1626
- highlightActiveElement.call(this, els[0], await this._getContext())
1627
- if (!Array.isArray(option)) option = [option]
1628
-
1629
- for (const key in option) {
1630
- const opt = xpathLocator.literal(option[key])
1631
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
1632
- if (optEl.length) {
1633
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1634
- continue
1635
- }
1636
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
1637
- if (optEl.length) {
1638
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1639
- }
1686
+ async selectOption(select, option, context = null) {
1687
+ const pageContext = await this._getContext()
1688
+ const matchedLocator = new Locator(select)
1689
+
1690
+ let contextEl
1691
+ if (context) {
1692
+ const contextEls = await findElements.call(this, pageContext, context)
1693
+ assertElementExists(contextEls, context, 'Context element')
1694
+ contextEl = contextEls[0]
1640
1695
  }
1641
- await this._evaluateHandeInContext(element => {
1642
- element.dispatchEvent(new Event('input', { bubbles: true }))
1643
- element.dispatchEvent(new Event('change', { bubbles: true }))
1644
- }, el)
1645
1696
 
1646
- return this._waitForAction()
1697
+ // Strict locator
1698
+ if (!matchedLocator.isFuzzy()) {
1699
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1700
+ const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1701
+ assertElementExists(els, select, 'Selectable element')
1702
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1703
+ }
1704
+
1705
+ // Fuzzy: try combobox
1706
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1707
+ const comboboxSearchCtx = contextEl || pageContext
1708
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1709
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1710
+
1711
+ // Fuzzy: try listbox
1712
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1713
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1714
+
1715
+ // Fuzzy: try native select
1716
+ const visibleEls = await findVisibleFields.call(this, select, context)
1717
+ assertElementExists(visibleEls, select, 'Selectable field')
1718
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1647
1719
  }
1648
1720
 
1649
1721
  /**
@@ -1687,6 +1759,26 @@ class Puppeteer extends Helper {
1687
1759
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
1688
1760
  }
1689
1761
 
1762
+ /**
1763
+ * {{> seeCurrentPathEquals }}
1764
+ */
1765
+ async seeCurrentPathEquals(path) {
1766
+ const currentUrl = await this._getPageUrl()
1767
+ const baseUrl = this.options.url || 'http://localhost'
1768
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1769
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1770
+ }
1771
+
1772
+ /**
1773
+ * {{> dontSeeCurrentPathEquals }}
1774
+ */
1775
+ async dontSeeCurrentPathEquals(path) {
1776
+ const currentUrl = await this._getPageUrl()
1777
+ const baseUrl = this.options.url || 'http://localhost'
1778
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1779
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1780
+ }
1781
+
1690
1782
  /**
1691
1783
  * {{> see }}
1692
1784
  *
@@ -2424,6 +2516,7 @@ class Puppeteer extends Helper {
2424
2516
  */
2425
2517
  async waitInUrl(urlPart, sec = null) {
2426
2518
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2519
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2427
2520
 
2428
2521
  return this.page
2429
2522
  .waitForFunction(
@@ -2432,12 +2525,12 @@ class Puppeteer extends Helper {
2432
2525
  return currUrl.indexOf(urlPart) > -1
2433
2526
  },
2434
2527
  { timeout: waitTimeout },
2435
- urlPart,
2528
+ expectedUrl,
2436
2529
  )
2437
2530
  .catch(async e => {
2438
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2531
+ const currUrl = await this._getPageUrl()
2439
2532
  if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2440
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2533
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2441
2534
  } else {
2442
2535
  throw e
2443
2536
  }
@@ -2449,25 +2542,50 @@ class Puppeteer extends Helper {
2449
2542
  */
2450
2543
  async waitUrlEquals(urlPart, sec = null) {
2451
2544
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2545
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2452
2546
 
2453
- const baseUrl = this.options.url
2454
- if (urlPart.indexOf('http') < 0) {
2455
- urlPart = baseUrl + urlPart
2456
- }
2547
+ return this.page
2548
+ .waitForFunction(
2549
+ url => {
2550
+ const currUrl = decodeURIComponent(window.location.href)
2551
+ return currUrl === url
2552
+ },
2553
+ { timeout: waitTimeout },
2554
+ expectedUrl,
2555
+ )
2556
+ .catch(async e => {
2557
+ const currUrl = await this._getPageUrl()
2558
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2559
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2560
+ } else {
2561
+ throw e
2562
+ }
2563
+ })
2564
+ }
2565
+
2566
+ /**
2567
+ * {{> waitCurrentPathEquals }}
2568
+ */
2569
+ async waitCurrentPathEquals(path, sec = null) {
2570
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2571
+ const normalizedPath = normalizePath(path)
2457
2572
 
2458
2573
  return this.page
2459
2574
  .waitForFunction(
2460
- urlPart => {
2461
- const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2462
- return currUrl.indexOf(urlPart) > -1
2575
+ expectedPath => {
2576
+ const actualPath = window.location.pathname
2577
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
2578
+ return normalizePath(actualPath) === expectedPath
2463
2579
  },
2464
2580
  { timeout: waitTimeout },
2465
- urlPart,
2581
+ normalizedPath,
2466
2582
  )
2467
2583
  .catch(async e => {
2468
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2584
+ const currUrl = await this._getPageUrl()
2585
+ const baseUrl = this.options.url || 'http://localhost'
2586
+ const actualPath = new URL(currUrl, baseUrl).pathname
2469
2587
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2470
- throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
2588
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2471
2589
  } else {
2472
2590
  throw e
2473
2591
  }
@@ -2895,10 +3013,17 @@ async function findElements(matcher, locator) {
2895
3013
  if (isReactLocator) return findReactElements.call(this, locator)
2896
3014
 
2897
3015
  locator = new Locator(locator, 'css')
2898
-
3016
+
2899
3017
  // Check if locator is a role locator and call findByRole
2900
3018
  if (locator.isRole()) return findByRole.call(this, matcher, locator)
2901
3019
 
3020
+ // Handle shadow DOM locators with >>> deep descendant combinator
3021
+ // { shadow: ['my-app', 'recipe-hello', 'button'] } => 'my-app >>> recipe-hello >>> button'
3022
+ if (locator.isShadow()) {
3023
+ const shadowSelector = locator.value.join(' >>> ')
3024
+ return matcher.$$(shadowSelector)
3025
+ }
3026
+
2902
3027
  // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
2903
3028
  if (!locator.isXPath()) return matcher.$$(locator.simplify())
2904
3029
 
@@ -2955,22 +3080,32 @@ async function findElements(matcher, locator) {
2955
3080
  async function findElement(matcher, locator) {
2956
3081
  if (locator.react) return findReactElements.call(this, locator)
2957
3082
  locator = new Locator(locator, 'css')
2958
-
3083
+
2959
3084
  // Check if locator is a role locator and call findByRole
2960
3085
  if (locator.isRole()) {
2961
3086
  const elements = await findByRole.call(this, matcher, locator)
2962
3087
  return elements[0]
2963
3088
  }
2964
3089
 
3090
+ // Handle shadow DOM locators with >>> deep descendant combinator
3091
+ if (locator.isShadow()) {
3092
+ const shadowSelector = locator.value.join(' >>> ')
3093
+ const elements = await matcher.$$(shadowSelector)
3094
+ return elements[0]
3095
+ }
3096
+
2965
3097
  // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2966
3098
  if (!locator.isXPath()) {
2967
3099
  const elements = await matcher.$$(locator.simplify())
2968
3100
  return elements[0]
2969
3101
  }
2970
-
2971
- // For XPath in Puppeteer 24.x+, use the same approach as findElements
2972
- // $x method was removed, so we use ::-p-xpath() or fallback
2973
- const elements = await findElements.call(this, matcher, locator)
3102
+ // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
3103
+ if (puppeteer.default?.defaultBrowserRevision) {
3104
+ const elements = await matcher.$$(`xpath/${locator.value}`)
3105
+ return elements[0]
3106
+ }
3107
+ // For Puppeteer 24.x+, $x method was removed - use ::-p-xpath() selector
3108
+ const elements = await matcher.$$(`::-p-xpath(${locator.value})`)
2974
3109
  return elements[0]
2975
3110
  }
2976
3111
 
@@ -2987,10 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
2987
3122
  } else {
2988
3123
  assertElementExists(els, locator, 'Clickable element')
2989
3124
  }
3125
+ const el = selectElement(els, locator, this)
2990
3126
 
2991
- highlightActiveElement.call(this, els[0], await this._getContext())
3127
+ highlightActiveElement.call(this, el, await this._getContext())
2992
3128
 
2993
- await els[0].click(options)
3129
+ await el.click(options)
2994
3130
  const promises = []
2995
3131
  if (options.waitForNavigation) {
2996
3132
  promises.push(this.waitForNavigation())
@@ -3121,43 +3257,57 @@ async function proceedIsChecked(assertType, option) {
3121
3257
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3122
3258
  }
3123
3259
 
3124
- async function findVisibleFields(locator) {
3125
- const els = await findFields.call(this, locator)
3260
+ async function findVisibleFields(locator, context = null) {
3261
+ const els = await findFields.call(this, locator, context)
3126
3262
  const visible = await Promise.all(els.map(el => el.boundingBox()))
3127
3263
  return els.filter((el, index) => visible[index])
3128
3264
  }
3129
3265
 
3130
- async function findFields(locator) {
3266
+ async function findFields(locator, context = null) {
3267
+ let contextEl
3268
+ if (context) {
3269
+ const contextPage = await this.context
3270
+ const contextEls = await findElements.call(this, contextPage, context)
3271
+ assertElementExists(contextEls, context, 'Context element')
3272
+ contextEl = contextEls[0]
3273
+ }
3274
+
3275
+ const locateFn = contextEl
3276
+ ? loc => findElements.call(this, contextEl, loc)
3277
+ : loc => this._locate(loc)
3278
+
3131
3279
  const matchedLocator = new Locator(locator)
3132
3280
  if (!matchedLocator.isFuzzy()) {
3133
- return this._locate(matchedLocator)
3281
+ return locateFn(matchedLocator)
3134
3282
  }
3135
3283
  const literal = xpathLocator.literal(matchedLocator.value)
3136
3284
 
3137
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
3285
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
3138
3286
  if (els.length) {
3139
3287
  return els
3140
3288
  }
3141
3289
 
3142
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
3290
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
3143
3291
  if (els.length) {
3144
3292
  return els
3145
3293
  }
3146
- els = await this._locate({ xpath: Locator.field.byName(literal) })
3294
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
3147
3295
  if (els.length) {
3148
3296
  return els
3149
3297
  }
3150
3298
 
3151
3299
  // Try ARIA selector for accessible name
3152
- try {
3153
- const page = await this.context
3154
- els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3155
- if (els.length) return els
3156
- } catch (err) {
3157
- // ARIA selector not supported or failed
3300
+ if (!contextEl) {
3301
+ try {
3302
+ const page = await this.context
3303
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3304
+ if (els.length) return els
3305
+ } catch (err) {
3306
+ // ARIA selector not supported or failed
3307
+ }
3158
3308
  }
3159
3309
 
3160
- return this._locate({ css: matchedLocator.value })
3310
+ return locateFn({ css: matchedLocator.value })
3161
3311
  }
3162
3312
 
3163
3313
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -3186,8 +3336,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3186
3336
  await this._waitForAction()
3187
3337
  }
3188
3338
 
3189
- async function proceedSeeInField(assertType, field, value) {
3190
- const els = await findVisibleFields.call(this, field)
3339
+ async function proceedSeeInField(assertType, field, value, context) {
3340
+ const els = await findVisibleFields.call(this, field, context)
3191
3341
  assertElementExists(els, field, 'Field')
3192
3342
  const el = els[0]
3193
3343
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
@@ -3300,6 +3450,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3300
3450
  }
3301
3451
  }
3302
3452
 
3453
+ function assertOnlyOneElement(elements, locator, helper) {
3454
+ if (elements.length > 1) {
3455
+ const webElements = elements.map(el => new WebElement(el, helper))
3456
+ throw new MultipleElementsFound(locator, webElements)
3457
+ }
3458
+ }
3459
+
3303
3460
  function $XPath(element, selector) {
3304
3461
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3305
3462
  const res = []
@@ -3538,3 +3695,81 @@ function createRoleTextMatcher(expected, exactMatch) {
3538
3695
  return value => typeof value === 'string' && value.includes(target)
3539
3696
  }
3540
3697
 
3698
+ async function proceedSelect(context, el, option) {
3699
+ const role = await el.evaluate(e => e.getAttribute('role'))
3700
+ const options = Array.isArray(option) ? option : [option]
3701
+
3702
+ if (role === 'combobox') {
3703
+ this.debugSection('SelectOption', 'Expanding combobox')
3704
+ highlightActiveElement.call(this, el, context)
3705
+ const ariaOwns = await el.evaluate(e => e.getAttribute('aria-owns'))
3706
+ const ariaControls = await el.evaluate(e => e.getAttribute('aria-controls'))
3707
+ await el.click()
3708
+ await this._waitForAction()
3709
+
3710
+ const listboxId = ariaOwns || ariaControls
3711
+ let listbox = null
3712
+ if (listboxId) {
3713
+ const listboxEls = await context.$$( `#${listboxId}`)
3714
+ if (listboxEls.length) listbox = listboxEls[0]
3715
+ }
3716
+ if (!listbox) {
3717
+ const listboxEls = await findByRole.call(this, context, { role: 'listbox' })
3718
+ if (listboxEls?.length) listbox = listboxEls[0]
3719
+ }
3720
+
3721
+ if (listbox) {
3722
+ for (const opt of options) {
3723
+ const optEls = await findByRole.call(this, listbox, { role: 'option', name: opt })
3724
+ if (optEls?.length) {
3725
+ const optEl = optEls[0]
3726
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3727
+ highlightActiveElement.call(this, optEl, context)
3728
+ await optEl.click()
3729
+ }
3730
+ }
3731
+ }
3732
+ return this._waitForAction()
3733
+ }
3734
+
3735
+ if (role === 'listbox') {
3736
+ for (const opt of options) {
3737
+ const optEls = await findByRole.call(this, el, { role: 'option', name: opt })
3738
+ if (optEls?.length) {
3739
+ const optEl = optEls[0]
3740
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3741
+ highlightActiveElement.call(this, optEl, context)
3742
+ await optEl.click()
3743
+ }
3744
+ }
3745
+ return this._waitForAction()
3746
+ }
3747
+
3748
+ // Native <select> element
3749
+ const tagName = await el.evaluate(e => e.tagName)
3750
+ if (tagName !== 'SELECT') {
3751
+ throw new Error('Element is not <select>')
3752
+ }
3753
+ highlightActiveElement.call(this, el, context)
3754
+ const optionArray = Array.isArray(option) ? option : [option]
3755
+
3756
+ for (const key in optionArray) {
3757
+ const opt = xpathLocator.literal(optionArray[key])
3758
+ let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
3759
+ if (optEl.length) {
3760
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
3761
+ continue
3762
+ }
3763
+ optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
3764
+ if (optEl.length) {
3765
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
3766
+ }
3767
+ }
3768
+ await this._evaluateHandeInContext(element => {
3769
+ element.dispatchEvent(new Event('input', { bubbles: true }))
3770
+ element.dispatchEvent(new Event('change', { bubbles: true }))
3771
+ }, el)
3772
+
3773
+ return this._waitForAction()
3774
+ }
3775
+