codeceptjs 4.0.0-rc.1 → 4.0.0-rc.11

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 (49) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +637 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/moveCursorTo.mustache +5 -1
  12. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  13. package/docs/webapi/seeElement.mustache +4 -0
  14. package/docs/webapi/seeInField.mustache +5 -0
  15. package/docs/webapi/selectOption.mustache +5 -0
  16. package/docs/webapi/uncheckOption.mustache +1 -1
  17. package/lib/codecept.js +20 -17
  18. package/lib/command/init.js +0 -3
  19. package/lib/command/run-workers.js +1 -0
  20. package/lib/container.js +19 -4
  21. package/lib/element/WebElement.js +81 -2
  22. package/lib/els.js +12 -6
  23. package/lib/helper/Appium.js +8 -8
  24. package/lib/helper/Playwright.js +224 -138
  25. package/lib/helper/Puppeteer.js +211 -69
  26. package/lib/helper/WebDriver.js +183 -64
  27. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  28. package/lib/helper/errors/NonFocusedType.js +8 -0
  29. package/lib/helper/extras/elementSelection.js +58 -0
  30. package/lib/helper/extras/focusCheck.js +43 -0
  31. package/lib/helper/scripts/dropFile.js +11 -0
  32. package/lib/html.js +14 -1
  33. package/lib/listener/globalRetry.js +32 -6
  34. package/lib/mocha/cli.js +10 -0
  35. package/lib/plugin/aiTrace.js +464 -0
  36. package/lib/plugin/retryFailedStep.js +28 -19
  37. package/lib/plugin/stepByStepReport.js +5 -1
  38. package/lib/step/config.js +15 -2
  39. package/lib/step/record.js +1 -1
  40. package/lib/utils.js +48 -0
  41. package/lib/workers.js +49 -7
  42. package/package.json +5 -3
  43. package/typings/index.d.ts +19 -0
  44. package/lib/listener/enhancedGlobalRetry.js +0 -110
  45. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  46. package/lib/plugin/htmlReporter.js +0 -3648
  47. package/lib/retryCoordinator.js +0 -207
  48. package/typings/promiseBasedTypes.d.ts +0 -9469
  49. package/typings/types.d.ts +0 -11402
@@ -8,6 +8,7 @@ import promiseRetry from 'promise-retry'
8
8
  import Locator from '../locator.js'
9
9
  import recorder from '../recorder.js'
10
10
  import store from '../store.js'
11
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
11
12
  import { includes as stringIncludes } from '../assert/include.js'
12
13
  import { urlEquals, equals } from '../assert/equal.js'
13
14
  import { empty } from '../assert/empty.js'
@@ -26,17 +27,24 @@ import {
26
27
  isModifierKey,
27
28
  requireWithFallback,
28
29
  normalizeSpacesInString,
30
+ normalizePath,
31
+ resolveUrl,
32
+ getMimeType,
33
+ base64EncodeFile,
29
34
  } from '../utils.js'
30
35
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
36
  import ElementNotFound from './errors/ElementNotFound.js'
37
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
32
38
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
33
39
  import Popup from './extras/Popup.js'
34
40
  import Console from './extras/Console.js'
35
41
  import { highlightElement } from './scripts/highlightElement.js'
36
42
  import { blurElement } from './scripts/blurElement.js'
43
+ import { dropFile } from './scripts/dropFile.js'
37
44
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
45
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
39
46
  import WebElement from '../element/WebElement.js'
47
+ import { selectElement } from './extras/elementSelection.js'
40
48
 
41
49
  let puppeteer
42
50
 
@@ -266,6 +274,7 @@ class Puppeteer extends Helper {
266
274
  show: false,
267
275
  defaultPopupAction: 'accept',
268
276
  highlightElement: false,
277
+ strict: false,
269
278
  }
270
279
 
271
280
  return Object.assign(defaults, config)
@@ -814,9 +823,26 @@ class Puppeteer extends Helper {
814
823
  * {{ react }}
815
824
  */
816
825
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
817
- const el = await this._locateElement(locator)
818
- if (!el) {
819
- throw new ElementNotFound(locator, 'Element to move cursor to')
826
+ let context = null
827
+ if (typeof offsetX !== 'number') {
828
+ context = offsetX
829
+ offsetX = 0
830
+ }
831
+
832
+ let el
833
+ if (context) {
834
+ const contextEls = await findElements.call(this, this.page, context)
835
+ assertElementExists(contextEls, context, 'Context element')
836
+ const els = await findElements.call(this, contextEls[0], locator)
837
+ if (!els || els.length === 0) {
838
+ throw new ElementNotFound(locator, 'Element to move cursor to')
839
+ }
840
+ el = els[0]
841
+ } else {
842
+ el = await this._locateElement(locator)
843
+ if (!el) {
844
+ throw new ElementNotFound(locator, 'Element to move cursor to')
845
+ }
820
846
  }
821
847
 
822
848
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -984,6 +1010,14 @@ class Puppeteer extends Helper {
984
1010
  */
985
1011
  async _locateElement(locator) {
986
1012
  const context = await this.context
1013
+ const elementIndex = store.currentStep?.opts?.elementIndex
1014
+ if (this.options.strict || elementIndex) {
1015
+ const elements = await findElements.call(this, context, locator)
1016
+ if (elements.length === 0) {
1017
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1018
+ }
1019
+ return selectElement(elements, locator, this)
1020
+ }
987
1021
  return findElement.call(this, context, locator)
988
1022
  }
989
1023
 
@@ -1001,7 +1035,7 @@ class Puppeteer extends Helper {
1001
1035
  if (!els || els.length === 0) {
1002
1036
  throw new ElementNotFound(locator, 'Checkbox or radio')
1003
1037
  }
1004
- return els[0]
1038
+ return selectElement(els, locator, this)
1005
1039
  }
1006
1040
 
1007
1041
  /**
@@ -1158,8 +1192,16 @@ class Puppeteer extends Helper {
1158
1192
  * {{> seeElement }}
1159
1193
  * {{ react }}
1160
1194
  */
1161
- async seeElement(locator) {
1162
- let els = await this._locate(locator)
1195
+ async seeElement(locator, context = null) {
1196
+ let els
1197
+ if (context) {
1198
+ const contextPage = await this.context
1199
+ const contextEls = await findElements.call(this, contextPage, context)
1200
+ assertElementExists(contextEls, context, 'Context element')
1201
+ els = await findElements.call(this, contextEls[0], locator)
1202
+ } else {
1203
+ els = await this._locate(locator)
1204
+ }
1163
1205
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1164
1206
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1165
1207
  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 +1216,16 @@ class Puppeteer extends Helper {
1174
1216
  * {{> dontSeeElement }}
1175
1217
  * {{ react }}
1176
1218
  */
1177
- async dontSeeElement(locator) {
1178
- let els = await this._locate(locator)
1219
+ async dontSeeElement(locator, context = null) {
1220
+ let els
1221
+ if (context) {
1222
+ const contextPage = await this.context
1223
+ const contextEls = await findElements.call(this, contextPage, context)
1224
+ assertElementExists(contextEls, context, 'Context element')
1225
+ els = await findElements.call(this, contextEls[0], locator)
1226
+ } else {
1227
+ els = await this._locate(locator)
1228
+ }
1179
1229
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1180
1230
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1181
1231
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1498,6 +1548,7 @@ class Puppeteer extends Helper {
1498
1548
  * {{> pressKeyWithKeyNormalization }}
1499
1549
  */
1500
1550
  async pressKey(key) {
1551
+ await checkFocusBeforePressKey(this, key)
1501
1552
  const modifiers = []
1502
1553
  if (Array.isArray(key)) {
1503
1554
  for (let k of key) {
@@ -1526,6 +1577,8 @@ class Puppeteer extends Helper {
1526
1577
  * {{> type }}
1527
1578
  */
1528
1579
  async type(keys, delay = null) {
1580
+ await checkFocusBeforeType(this)
1581
+
1529
1582
  if (!Array.isArray(keys)) {
1530
1583
  keys = keys.toString()
1531
1584
  keys = keys.split('')
@@ -1541,10 +1594,10 @@ class Puppeteer extends Helper {
1541
1594
  * {{> fillField }}
1542
1595
  * {{ react }}
1543
1596
  */
1544
- async fillField(field, value) {
1545
- const els = await findVisibleFields.call(this, field)
1597
+ async fillField(field, value, context = null) {
1598
+ const els = await findVisibleFields.call(this, field, context)
1546
1599
  assertElementExists(els, field, 'Field')
1547
- const el = els[0]
1600
+ const el = selectElement(els, field, this)
1548
1601
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1549
1602
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1550
1603
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1562,8 +1615,8 @@ class Puppeteer extends Helper {
1562
1615
  /**
1563
1616
  * {{> clearField }}
1564
1617
  */
1565
- async clearField(field) {
1566
- return this.fillField(field, '')
1618
+ async clearField(field, context = null) {
1619
+ return this.fillField(field, '', context)
1567
1620
  }
1568
1621
 
1569
1622
  /**
@@ -1571,29 +1624,30 @@ class Puppeteer extends Helper {
1571
1624
  *
1572
1625
  * {{ react }}
1573
1626
  */
1574
- async appendField(field, value) {
1575
- const els = await findVisibleFields.call(this, field)
1627
+ async appendField(field, value, context = null) {
1628
+ const els = await findVisibleFields.call(this, field, context)
1576
1629
  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 })
1630
+ const el = selectElement(els, field, this)
1631
+ highlightActiveElement.call(this, el, await this._getContext())
1632
+ await el.press('End')
1633
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1580
1634
  return this._waitForAction()
1581
1635
  }
1582
1636
 
1583
1637
  /**
1584
1638
  * {{> seeInField }}
1585
1639
  */
1586
- async seeInField(field, value) {
1640
+ async seeInField(field, value, context = null) {
1587
1641
  const _value = typeof value === 'boolean' ? value : value.toString()
1588
- return proceedSeeInField.call(this, 'assert', field, _value)
1642
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
1589
1643
  }
1590
1644
 
1591
1645
  /**
1592
1646
  * {{> dontSeeInField }}
1593
1647
  */
1594
- async dontSeeInField(field, value) {
1648
+ async dontSeeInField(field, value, context = null) {
1595
1649
  const _value = typeof value === 'boolean' ? value : value.toString()
1596
- return proceedSeeInField.call(this, 'negate', field, _value)
1650
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
1597
1651
  }
1598
1652
 
1599
1653
  /**
@@ -1601,46 +1655,71 @@ class Puppeteer extends Helper {
1601
1655
  *
1602
1656
  * {{> attachFile }}
1603
1657
  */
1604
- async attachFile(locator, pathToFile) {
1658
+ async attachFile(locator, pathToFile, context = null) {
1605
1659
  const file = path.join(global.codecept_dir, pathToFile)
1606
1660
 
1607
1661
  if (!fileExists(file)) {
1608
1662
  throw new Error(`File at ${file} can not be found on local system`)
1609
1663
  }
1610
- const els = await findFields.call(this, locator)
1611
- assertElementExists(els, locator, 'Field')
1612
- await els[0].uploadFile(file)
1664
+ const els = await findFields.call(this, locator, context)
1665
+ if (els.length) {
1666
+ const el = selectElement(els, locator, this)
1667
+ const tag = await el.evaluate(el => el.tagName)
1668
+ const type = await el.evaluate(el => el.type)
1669
+ if (tag === 'INPUT' && type === 'file') {
1670
+ await el.uploadFile(file)
1671
+ return this._waitForAction()
1672
+ }
1673
+ }
1674
+
1675
+ const targetEls = els.length ? els : await this._locate(locator)
1676
+ assertElementExists(targetEls, locator, 'Element')
1677
+ const el = selectElement(targetEls, locator, this)
1678
+ const fileData = {
1679
+ base64Content: base64EncodeFile(file),
1680
+ fileName: path.basename(file),
1681
+ mimeType: getMimeType(path.basename(file)),
1682
+ }
1683
+ await el.evaluate(dropFile, fileData)
1613
1684
  return this._waitForAction()
1614
1685
  }
1615
1686
 
1616
1687
  /**
1617
1688
  * {{> selectOption }}
1618
1689
  */
1619
- async selectOption(select, option) {
1620
- const context = await this._getContext()
1690
+ async selectOption(select, option, context = null) {
1691
+ const pageContext = await this._getContext()
1621
1692
  const matchedLocator = new Locator(select)
1622
1693
 
1694
+ let contextEl
1695
+ if (context) {
1696
+ const contextEls = await findElements.call(this, pageContext, context)
1697
+ assertElementExists(contextEls, context, 'Context element')
1698
+ contextEl = contextEls[0]
1699
+ }
1700
+
1623
1701
  // Strict locator
1624
1702
  if (!matchedLocator.isFuzzy()) {
1625
1703
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1626
- const els = await this._locate(select)
1704
+ const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1627
1705
  assertElementExists(els, select, 'Selectable element')
1628
- return proceedSelect.call(this, context, els[0], option)
1706
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1629
1707
  }
1630
1708
 
1631
1709
  // Fuzzy: try combobox
1632
1710
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1633
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
1634
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1711
+ const comboboxSearchCtx = contextEl || pageContext
1712
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1713
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1635
1714
 
1636
1715
  // Fuzzy: try listbox
1637
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
1638
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1716
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1717
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1639
1718
 
1640
1719
  // Fuzzy: try native select
1641
- const visibleEls = await findVisibleFields.call(this, select)
1720
+ const visibleEls = await findVisibleFields.call(this, select, context)
1642
1721
  assertElementExists(visibleEls, select, 'Selectable field')
1643
- return proceedSelect.call(this, context, visibleEls[0], option)
1722
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1644
1723
  }
1645
1724
 
1646
1725
  /**
@@ -1684,6 +1763,26 @@ class Puppeteer extends Helper {
1684
1763
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
1685
1764
  }
1686
1765
 
1766
+ /**
1767
+ * {{> seeCurrentPathEquals }}
1768
+ */
1769
+ async seeCurrentPathEquals(path) {
1770
+ const currentUrl = await this._getPageUrl()
1771
+ const baseUrl = this.options.url || 'http://localhost'
1772
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1773
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1774
+ }
1775
+
1776
+ /**
1777
+ * {{> dontSeeCurrentPathEquals }}
1778
+ */
1779
+ async dontSeeCurrentPathEquals(path) {
1780
+ const currentUrl = await this._getPageUrl()
1781
+ const baseUrl = this.options.url || 'http://localhost'
1782
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1783
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1784
+ }
1785
+
1687
1786
  /**
1688
1787
  * {{> see }}
1689
1788
  *
@@ -2421,6 +2520,7 @@ class Puppeteer extends Helper {
2421
2520
  */
2422
2521
  async waitInUrl(urlPart, sec = null) {
2423
2522
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2523
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2424
2524
 
2425
2525
  return this.page
2426
2526
  .waitForFunction(
@@ -2429,12 +2529,12 @@ class Puppeteer extends Helper {
2429
2529
  return currUrl.indexOf(urlPart) > -1
2430
2530
  },
2431
2531
  { timeout: waitTimeout },
2432
- urlPart,
2532
+ expectedUrl,
2433
2533
  )
2434
2534
  .catch(async e => {
2435
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2535
+ const currUrl = await this._getPageUrl()
2436
2536
  if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2437
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2537
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2438
2538
  } else {
2439
2539
  throw e
2440
2540
  }
@@ -2446,18 +2546,13 @@ class Puppeteer extends Helper {
2446
2546
  */
2447
2547
  async waitUrlEquals(urlPart, sec = null) {
2448
2548
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2449
-
2450
- const baseUrl = this.options.url
2451
- let expectedUrl = urlPart
2452
- if (urlPart.indexOf('http') < 0) {
2453
- expectedUrl = baseUrl + urlPart
2454
- }
2549
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2455
2550
 
2456
2551
  return this.page
2457
2552
  .waitForFunction(
2458
2553
  url => {
2459
2554
  const currUrl = decodeURIComponent(window.location.href)
2460
- return currUrl.indexOf(url) > -1
2555
+ return currUrl === url
2461
2556
  },
2462
2557
  { timeout: waitTimeout },
2463
2558
  expectedUrl,
@@ -2465,11 +2560,36 @@ class Puppeteer extends Helper {
2465
2560
  .catch(async e => {
2466
2561
  const currUrl = await this._getPageUrl()
2467
2562
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2468
- if (!currUrl.includes(expectedUrl)) {
2469
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2470
- } else {
2471
- throw new Error(`expected url not loaded, error message: ${e.message}`)
2472
- }
2563
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2564
+ } else {
2565
+ throw e
2566
+ }
2567
+ })
2568
+ }
2569
+
2570
+ /**
2571
+ * {{> waitCurrentPathEquals }}
2572
+ */
2573
+ async waitCurrentPathEquals(path, sec = null) {
2574
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2575
+ const normalizedPath = normalizePath(path)
2576
+
2577
+ return this.page
2578
+ .waitForFunction(
2579
+ expectedPath => {
2580
+ const actualPath = window.location.pathname
2581
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
2582
+ return normalizePath(actualPath) === expectedPath
2583
+ },
2584
+ { timeout: waitTimeout },
2585
+ normalizedPath,
2586
+ )
2587
+ .catch(async e => {
2588
+ const currUrl = await this._getPageUrl()
2589
+ const baseUrl = this.options.url || 'http://localhost'
2590
+ const actualPath = new URL(currUrl, baseUrl).pathname
2591
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2592
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2473
2593
  } else {
2474
2594
  throw e
2475
2595
  }
@@ -3006,10 +3126,11 @@ async function proceedClick(locator, context = null, options = {}) {
3006
3126
  } else {
3007
3127
  assertElementExists(els, locator, 'Clickable element')
3008
3128
  }
3129
+ const el = selectElement(els, locator, this)
3009
3130
 
3010
- highlightActiveElement.call(this, els[0], await this._getContext())
3131
+ highlightActiveElement.call(this, el, await this._getContext())
3011
3132
 
3012
- await els[0].click(options)
3133
+ await el.click(options)
3013
3134
  const promises = []
3014
3135
  if (options.waitForNavigation) {
3015
3136
  promises.push(this.waitForNavigation())
@@ -3140,43 +3261,57 @@ async function proceedIsChecked(assertType, option) {
3140
3261
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3141
3262
  }
3142
3263
 
3143
- async function findVisibleFields(locator) {
3144
- const els = await findFields.call(this, locator)
3264
+ async function findVisibleFields(locator, context = null) {
3265
+ const els = await findFields.call(this, locator, context)
3145
3266
  const visible = await Promise.all(els.map(el => el.boundingBox()))
3146
3267
  return els.filter((el, index) => visible[index])
3147
3268
  }
3148
3269
 
3149
- async function findFields(locator) {
3270
+ async function findFields(locator, context = null) {
3271
+ let contextEl
3272
+ if (context) {
3273
+ const contextPage = await this.context
3274
+ const contextEls = await findElements.call(this, contextPage, context)
3275
+ assertElementExists(contextEls, context, 'Context element')
3276
+ contextEl = contextEls[0]
3277
+ }
3278
+
3279
+ const locateFn = contextEl
3280
+ ? loc => findElements.call(this, contextEl, loc)
3281
+ : loc => this._locate(loc)
3282
+
3150
3283
  const matchedLocator = new Locator(locator)
3151
3284
  if (!matchedLocator.isFuzzy()) {
3152
- return this._locate(matchedLocator)
3285
+ return locateFn(matchedLocator)
3153
3286
  }
3154
3287
  const literal = xpathLocator.literal(matchedLocator.value)
3155
3288
 
3156
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
3289
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
3157
3290
  if (els.length) {
3158
3291
  return els
3159
3292
  }
3160
3293
 
3161
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
3294
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
3162
3295
  if (els.length) {
3163
3296
  return els
3164
3297
  }
3165
- els = await this._locate({ xpath: Locator.field.byName(literal) })
3298
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
3166
3299
  if (els.length) {
3167
3300
  return els
3168
3301
  }
3169
3302
 
3170
3303
  // Try ARIA selector for accessible name
3171
- try {
3172
- const page = await this.context
3173
- els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3174
- if (els.length) return els
3175
- } catch (err) {
3176
- // ARIA selector not supported or failed
3304
+ if (!contextEl) {
3305
+ try {
3306
+ const page = await this.context
3307
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3308
+ if (els.length) return els
3309
+ } catch (err) {
3310
+ // ARIA selector not supported or failed
3311
+ }
3177
3312
  }
3178
3313
 
3179
- return this._locate({ css: matchedLocator.value })
3314
+ return locateFn({ css: matchedLocator.value })
3180
3315
  }
3181
3316
 
3182
3317
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -3205,8 +3340,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3205
3340
  await this._waitForAction()
3206
3341
  }
3207
3342
 
3208
- async function proceedSeeInField(assertType, field, value) {
3209
- const els = await findVisibleFields.call(this, field)
3343
+ async function proceedSeeInField(assertType, field, value, context) {
3344
+ const els = await findVisibleFields.call(this, field, context)
3210
3345
  assertElementExists(els, field, 'Field')
3211
3346
  const el = els[0]
3212
3347
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
@@ -3319,6 +3454,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3319
3454
  }
3320
3455
  }
3321
3456
 
3457
+ function assertOnlyOneElement(elements, locator, helper) {
3458
+ if (elements.length > 1) {
3459
+ const webElements = elements.map(el => new WebElement(el, helper))
3460
+ throw new MultipleElementsFound(locator, webElements)
3461
+ }
3462
+ }
3463
+
3322
3464
  function $XPath(element, selector) {
3323
3465
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3324
3466
  const res = []