codeceptjs 4.0.0-rc.2 → 4.0.0-rc.8
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.
- package/README.md +39 -27
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/codecept.js +20 -17
- package/lib/command/init.js +0 -3
- package/lib/command/run-workers.js +1 -0
- package/lib/container.js +19 -4
- package/lib/element/WebElement.js +52 -0
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/Playwright.js +169 -87
- package/lib/helper/Puppeteer.js +181 -64
- package/lib/helper/WebDriver.js +141 -53
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/mocha/cli.js +10 -0
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/utils.js +48 -0
- package/lib/workers.js +49 -7
- package/package.json +5 -3
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -26,14 +26,20 @@ 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'
|
|
@@ -266,6 +272,7 @@ class Puppeteer extends Helper {
|
|
|
266
272
|
show: false,
|
|
267
273
|
defaultPopupAction: 'accept',
|
|
268
274
|
highlightElement: false,
|
|
275
|
+
strict: false,
|
|
269
276
|
}
|
|
270
277
|
|
|
271
278
|
return Object.assign(defaults, config)
|
|
@@ -814,9 +821,26 @@ class Puppeteer extends Helper {
|
|
|
814
821
|
* {{ react }}
|
|
815
822
|
*/
|
|
816
823
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
824
|
+
let context = null
|
|
825
|
+
if (typeof offsetX !== 'number') {
|
|
826
|
+
context = offsetX
|
|
827
|
+
offsetX = 0
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let el
|
|
831
|
+
if (context) {
|
|
832
|
+
const contextEls = await findElements.call(this, this.page, context)
|
|
833
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
834
|
+
const els = await findElements.call(this, contextEls[0], locator)
|
|
835
|
+
if (!els || els.length === 0) {
|
|
836
|
+
throw new ElementNotFound(locator, 'Element to move cursor to')
|
|
837
|
+
}
|
|
838
|
+
el = els[0]
|
|
839
|
+
} else {
|
|
840
|
+
el = await this._locateElement(locator)
|
|
841
|
+
if (!el) {
|
|
842
|
+
throw new ElementNotFound(locator, 'Element to move cursor to')
|
|
843
|
+
}
|
|
820
844
|
}
|
|
821
845
|
|
|
822
846
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
@@ -984,6 +1008,14 @@ class Puppeteer extends Helper {
|
|
|
984
1008
|
*/
|
|
985
1009
|
async _locateElement(locator) {
|
|
986
1010
|
const context = await this.context
|
|
1011
|
+
if (this.options.strict) {
|
|
1012
|
+
const elements = await findElements.call(this, context, locator)
|
|
1013
|
+
if (elements.length === 0) {
|
|
1014
|
+
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1015
|
+
}
|
|
1016
|
+
assertOnlyOneElement(elements, locator, this)
|
|
1017
|
+
return elements[0]
|
|
1018
|
+
}
|
|
987
1019
|
return findElement.call(this, context, locator)
|
|
988
1020
|
}
|
|
989
1021
|
|
|
@@ -1001,6 +1033,7 @@ class Puppeteer extends Helper {
|
|
|
1001
1033
|
if (!els || els.length === 0) {
|
|
1002
1034
|
throw new ElementNotFound(locator, 'Checkbox or radio')
|
|
1003
1035
|
}
|
|
1036
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
1004
1037
|
return els[0]
|
|
1005
1038
|
}
|
|
1006
1039
|
|
|
@@ -1158,8 +1191,16 @@ class Puppeteer extends Helper {
|
|
|
1158
1191
|
* {{> seeElement }}
|
|
1159
1192
|
* {{ react }}
|
|
1160
1193
|
*/
|
|
1161
|
-
async seeElement(locator) {
|
|
1162
|
-
let els
|
|
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
|
|
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,9 +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')
|
|
1596
|
+
if (this.options.strict) assertOnlyOneElement(els, field, this)
|
|
1547
1597
|
const el = els[0]
|
|
1548
1598
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1549
1599
|
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
@@ -1562,8 +1612,8 @@ class Puppeteer extends Helper {
|
|
|
1562
1612
|
/**
|
|
1563
1613
|
* {{> clearField }}
|
|
1564
1614
|
*/
|
|
1565
|
-
async clearField(field) {
|
|
1566
|
-
return this.fillField(field, '')
|
|
1615
|
+
async clearField(field, context = null) {
|
|
1616
|
+
return this.fillField(field, '', context)
|
|
1567
1617
|
}
|
|
1568
1618
|
|
|
1569
1619
|
/**
|
|
@@ -1571,9 +1621,10 @@ class Puppeteer extends Helper {
|
|
|
1571
1621
|
*
|
|
1572
1622
|
* {{ react }}
|
|
1573
1623
|
*/
|
|
1574
|
-
async appendField(field, value) {
|
|
1575
|
-
const els = await findVisibleFields.call(this, field)
|
|
1624
|
+
async appendField(field, value, context = null) {
|
|
1625
|
+
const els = await findVisibleFields.call(this, field, context)
|
|
1576
1626
|
assertElementExists(els, field, 'Field')
|
|
1627
|
+
if (this.options.strict) assertOnlyOneElement(els, field, this)
|
|
1577
1628
|
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
1578
1629
|
await els[0].press('End')
|
|
1579
1630
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
@@ -1583,17 +1634,17 @@ class Puppeteer extends Helper {
|
|
|
1583
1634
|
/**
|
|
1584
1635
|
* {{> seeInField }}
|
|
1585
1636
|
*/
|
|
1586
|
-
async seeInField(field, value) {
|
|
1637
|
+
async seeInField(field, value, context = null) {
|
|
1587
1638
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1588
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
1639
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
1589
1640
|
}
|
|
1590
1641
|
|
|
1591
1642
|
/**
|
|
1592
1643
|
* {{> dontSeeInField }}
|
|
1593
1644
|
*/
|
|
1594
|
-
async dontSeeInField(field, value) {
|
|
1645
|
+
async dontSeeInField(field, value, context = null) {
|
|
1595
1646
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1596
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
1647
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
1597
1648
|
}
|
|
1598
1649
|
|
|
1599
1650
|
/**
|
|
@@ -1601,46 +1652,69 @@ class Puppeteer extends Helper {
|
|
|
1601
1652
|
*
|
|
1602
1653
|
* {{> attachFile }}
|
|
1603
1654
|
*/
|
|
1604
|
-
async attachFile(locator, pathToFile) {
|
|
1655
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1605
1656
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
1606
1657
|
|
|
1607
1658
|
if (!fileExists(file)) {
|
|
1608
1659
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1609
1660
|
}
|
|
1610
|
-
const els = await findFields.call(this, locator)
|
|
1611
|
-
|
|
1612
|
-
|
|
1661
|
+
const els = await findFields.call(this, locator, context)
|
|
1662
|
+
if (els.length) {
|
|
1663
|
+
const tag = await els[0].evaluate(el => el.tagName)
|
|
1664
|
+
const type = await els[0].evaluate(el => el.type)
|
|
1665
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
1666
|
+
await els[0].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 fileData = {
|
|
1674
|
+
base64Content: base64EncodeFile(file),
|
|
1675
|
+
fileName: path.basename(file),
|
|
1676
|
+
mimeType: getMimeType(path.basename(file)),
|
|
1677
|
+
}
|
|
1678
|
+
await targetEls[0].evaluate(dropFile, fileData)
|
|
1613
1679
|
return this._waitForAction()
|
|
1614
1680
|
}
|
|
1615
1681
|
|
|
1616
1682
|
/**
|
|
1617
1683
|
* {{> selectOption }}
|
|
1618
1684
|
*/
|
|
1619
|
-
async selectOption(select, option) {
|
|
1620
|
-
const
|
|
1685
|
+
async selectOption(select, option, context = null) {
|
|
1686
|
+
const pageContext = await this._getContext()
|
|
1621
1687
|
const matchedLocator = new Locator(select)
|
|
1622
1688
|
|
|
1689
|
+
let contextEl
|
|
1690
|
+
if (context) {
|
|
1691
|
+
const contextEls = await findElements.call(this, pageContext, context)
|
|
1692
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1693
|
+
contextEl = contextEls[0]
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1623
1696
|
// Strict locator
|
|
1624
1697
|
if (!matchedLocator.isFuzzy()) {
|
|
1625
1698
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1626
|
-
const els = await this._locate(select)
|
|
1699
|
+
const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
|
|
1627
1700
|
assertElementExists(els, select, 'Selectable element')
|
|
1628
|
-
return proceedSelect.call(this,
|
|
1701
|
+
return proceedSelect.call(this, pageContext, els[0], option)
|
|
1629
1702
|
}
|
|
1630
1703
|
|
|
1631
1704
|
// Fuzzy: try combobox
|
|
1632
1705
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1633
|
-
|
|
1634
|
-
|
|
1706
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
1707
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
1708
|
+
if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
|
|
1635
1709
|
|
|
1636
1710
|
// Fuzzy: try listbox
|
|
1637
|
-
els = await findByRole(
|
|
1638
|
-
if (els?.length) return proceedSelect.call(this,
|
|
1711
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
1712
|
+
if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
|
|
1639
1713
|
|
|
1640
1714
|
// Fuzzy: try native select
|
|
1641
|
-
const visibleEls = await findVisibleFields.call(this, select)
|
|
1715
|
+
const visibleEls = await findVisibleFields.call(this, select, context)
|
|
1642
1716
|
assertElementExists(visibleEls, select, 'Selectable field')
|
|
1643
|
-
return proceedSelect.call(this,
|
|
1717
|
+
return proceedSelect.call(this, pageContext, visibleEls[0], option)
|
|
1644
1718
|
}
|
|
1645
1719
|
|
|
1646
1720
|
/**
|
|
@@ -1691,7 +1765,7 @@ class Puppeteer extends Helper {
|
|
|
1691
1765
|
const currentUrl = await this._getPageUrl()
|
|
1692
1766
|
const baseUrl = this.options.url || 'http://localhost'
|
|
1693
1767
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1694
|
-
return equals('url path').assert(path, actualPath)
|
|
1768
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
1695
1769
|
}
|
|
1696
1770
|
|
|
1697
1771
|
/**
|
|
@@ -1701,7 +1775,7 @@ class Puppeteer extends Helper {
|
|
|
1701
1775
|
const currentUrl = await this._getPageUrl()
|
|
1702
1776
|
const baseUrl = this.options.url || 'http://localhost'
|
|
1703
1777
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1704
|
-
return equals('url path').negate(path, actualPath)
|
|
1778
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
1705
1779
|
}
|
|
1706
1780
|
|
|
1707
1781
|
/**
|
|
@@ -2441,6 +2515,7 @@ class Puppeteer extends Helper {
|
|
|
2441
2515
|
*/
|
|
2442
2516
|
async waitInUrl(urlPart, sec = null) {
|
|
2443
2517
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2518
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2444
2519
|
|
|
2445
2520
|
return this.page
|
|
2446
2521
|
.waitForFunction(
|
|
@@ -2449,12 +2524,12 @@ class Puppeteer extends Helper {
|
|
|
2449
2524
|
return currUrl.indexOf(urlPart) > -1
|
|
2450
2525
|
},
|
|
2451
2526
|
{ timeout: waitTimeout },
|
|
2452
|
-
|
|
2527
|
+
expectedUrl,
|
|
2453
2528
|
)
|
|
2454
2529
|
.catch(async e => {
|
|
2455
|
-
const currUrl = await this._getPageUrl()
|
|
2530
|
+
const currUrl = await this._getPageUrl()
|
|
2456
2531
|
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2457
|
-
throw new Error(`expected url to include ${
|
|
2532
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2458
2533
|
} else {
|
|
2459
2534
|
throw e
|
|
2460
2535
|
}
|
|
@@ -2466,18 +2541,13 @@ class Puppeteer extends Helper {
|
|
|
2466
2541
|
*/
|
|
2467
2542
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2468
2543
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2469
|
-
|
|
2470
|
-
const baseUrl = this.options.url
|
|
2471
|
-
let expectedUrl = urlPart
|
|
2472
|
-
if (urlPart.indexOf('http') < 0) {
|
|
2473
|
-
expectedUrl = baseUrl + urlPart
|
|
2474
|
-
}
|
|
2544
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2475
2545
|
|
|
2476
2546
|
return this.page
|
|
2477
2547
|
.waitForFunction(
|
|
2478
2548
|
url => {
|
|
2479
2549
|
const currUrl = decodeURIComponent(window.location.href)
|
|
2480
|
-
return currUrl
|
|
2550
|
+
return currUrl === url
|
|
2481
2551
|
},
|
|
2482
2552
|
{ timeout: waitTimeout },
|
|
2483
2553
|
expectedUrl,
|
|
@@ -2485,11 +2555,36 @@ class Puppeteer extends Helper {
|
|
|
2485
2555
|
.catch(async e => {
|
|
2486
2556
|
const currUrl = await this._getPageUrl()
|
|
2487
2557
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2558
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
2559
|
+
} else {
|
|
2560
|
+
throw e
|
|
2561
|
+
}
|
|
2562
|
+
})
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* {{> waitCurrentPathEquals }}
|
|
2567
|
+
*/
|
|
2568
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
2569
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2570
|
+
const normalizedPath = normalizePath(path)
|
|
2571
|
+
|
|
2572
|
+
return this.page
|
|
2573
|
+
.waitForFunction(
|
|
2574
|
+
expectedPath => {
|
|
2575
|
+
const actualPath = window.location.pathname
|
|
2576
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
2577
|
+
return normalizePath(actualPath) === expectedPath
|
|
2578
|
+
},
|
|
2579
|
+
{ timeout: waitTimeout },
|
|
2580
|
+
normalizedPath,
|
|
2581
|
+
)
|
|
2582
|
+
.catch(async e => {
|
|
2583
|
+
const currUrl = await this._getPageUrl()
|
|
2584
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2585
|
+
const actualPath = new URL(currUrl, baseUrl).pathname
|
|
2586
|
+
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2587
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2493
2588
|
} else {
|
|
2494
2589
|
throw e
|
|
2495
2590
|
}
|
|
@@ -3026,6 +3121,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3026
3121
|
} else {
|
|
3027
3122
|
assertElementExists(els, locator, 'Clickable element')
|
|
3028
3123
|
}
|
|
3124
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
3029
3125
|
|
|
3030
3126
|
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
3031
3127
|
|
|
@@ -3160,43 +3256,57 @@ async function proceedIsChecked(assertType, option) {
|
|
|
3160
3256
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
3161
3257
|
}
|
|
3162
3258
|
|
|
3163
|
-
async function findVisibleFields(locator) {
|
|
3164
|
-
const els = await findFields.call(this, locator)
|
|
3259
|
+
async function findVisibleFields(locator, context = null) {
|
|
3260
|
+
const els = await findFields.call(this, locator, context)
|
|
3165
3261
|
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
3166
3262
|
return els.filter((el, index) => visible[index])
|
|
3167
3263
|
}
|
|
3168
3264
|
|
|
3169
|
-
async function findFields(locator) {
|
|
3265
|
+
async function findFields(locator, context = null) {
|
|
3266
|
+
let contextEl
|
|
3267
|
+
if (context) {
|
|
3268
|
+
const contextPage = await this.context
|
|
3269
|
+
const contextEls = await findElements.call(this, contextPage, context)
|
|
3270
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
3271
|
+
contextEl = contextEls[0]
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
const locateFn = contextEl
|
|
3275
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
3276
|
+
: loc => this._locate(loc)
|
|
3277
|
+
|
|
3170
3278
|
const matchedLocator = new Locator(locator)
|
|
3171
3279
|
if (!matchedLocator.isFuzzy()) {
|
|
3172
|
-
return
|
|
3280
|
+
return locateFn(matchedLocator)
|
|
3173
3281
|
}
|
|
3174
3282
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3175
3283
|
|
|
3176
|
-
let els = await
|
|
3284
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
3177
3285
|
if (els.length) {
|
|
3178
3286
|
return els
|
|
3179
3287
|
}
|
|
3180
3288
|
|
|
3181
|
-
els = await
|
|
3289
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
3182
3290
|
if (els.length) {
|
|
3183
3291
|
return els
|
|
3184
3292
|
}
|
|
3185
|
-
els = await
|
|
3293
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
3186
3294
|
if (els.length) {
|
|
3187
3295
|
return els
|
|
3188
3296
|
}
|
|
3189
3297
|
|
|
3190
3298
|
// Try ARIA selector for accessible name
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3299
|
+
if (!contextEl) {
|
|
3300
|
+
try {
|
|
3301
|
+
const page = await this.context
|
|
3302
|
+
els = await page.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3303
|
+
if (els.length) return els
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
// ARIA selector not supported or failed
|
|
3306
|
+
}
|
|
3197
3307
|
}
|
|
3198
3308
|
|
|
3199
|
-
return
|
|
3309
|
+
return locateFn({ css: matchedLocator.value })
|
|
3200
3310
|
}
|
|
3201
3311
|
|
|
3202
3312
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
@@ -3225,8 +3335,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
|
3225
3335
|
await this._waitForAction()
|
|
3226
3336
|
}
|
|
3227
3337
|
|
|
3228
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
3229
|
-
const els = await findVisibleFields.call(this, field)
|
|
3338
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
3339
|
+
const els = await findVisibleFields.call(this, field, context)
|
|
3230
3340
|
assertElementExists(els, field, 'Field')
|
|
3231
3341
|
const el = els[0]
|
|
3232
3342
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
@@ -3339,6 +3449,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3339
3449
|
}
|
|
3340
3450
|
}
|
|
3341
3451
|
|
|
3452
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
3453
|
+
if (elements.length > 1) {
|
|
3454
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
3455
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3342
3459
|
function $XPath(element, selector) {
|
|
3343
3460
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
3344
3461
|
const res = []
|