codeceptjs 4.0.0-rc.1 → 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.
- 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/dontSeeCurrentPathEquals.mustache +10 -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/seeCurrentPathEquals.mustache +10 -0
- 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 +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/Playwright.js +219 -137
- package/lib/helper/Puppeteer.js +207 -69
- package/lib/helper/WebDriver.js +179 -64
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/extras/elementSelection.js +58 -0
- 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/step/config.js +15 -2
- package/lib/step/record.js +1 -1
- package/lib/utils.js +48 -0
- package/lib/workers.js +49 -7
- package/package.json +5 -3
- package/typings/index.d.ts +19 -0
- 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,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
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
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
|
|
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
|
|
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,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
|
|
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
|
-
|
|
1578
|
-
await
|
|
1579
|
-
await
|
|
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,46 +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
|
-
|
|
1612
|
-
|
|
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
|
|
1686
|
+
async selectOption(select, option, context = null) {
|
|
1687
|
+
const pageContext = await this._getContext()
|
|
1621
1688
|
const matchedLocator = new Locator(select)
|
|
1622
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]
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1623
1697
|
// Strict locator
|
|
1624
1698
|
if (!matchedLocator.isFuzzy()) {
|
|
1625
1699
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1626
|
-
const els = await this._locate(select)
|
|
1700
|
+
const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
|
|
1627
1701
|
assertElementExists(els, select, 'Selectable element')
|
|
1628
|
-
return proceedSelect.call(this,
|
|
1702
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1629
1703
|
}
|
|
1630
1704
|
|
|
1631
1705
|
// Fuzzy: try combobox
|
|
1632
1706
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1633
|
-
|
|
1634
|
-
|
|
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)
|
|
1635
1710
|
|
|
1636
1711
|
// Fuzzy: try listbox
|
|
1637
|
-
els = await findByRole(
|
|
1638
|
-
if (els?.length) return proceedSelect.call(this,
|
|
1712
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
1713
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1639
1714
|
|
|
1640
1715
|
// Fuzzy: try native select
|
|
1641
|
-
const visibleEls = await findVisibleFields.call(this, select)
|
|
1716
|
+
const visibleEls = await findVisibleFields.call(this, select, context)
|
|
1642
1717
|
assertElementExists(visibleEls, select, 'Selectable field')
|
|
1643
|
-
return proceedSelect.call(this,
|
|
1718
|
+
return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
|
|
1644
1719
|
}
|
|
1645
1720
|
|
|
1646
1721
|
/**
|
|
@@ -1684,6 +1759,26 @@ class Puppeteer extends Helper {
|
|
|
1684
1759
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
1685
1760
|
}
|
|
1686
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
|
+
|
|
1687
1782
|
/**
|
|
1688
1783
|
* {{> see }}
|
|
1689
1784
|
*
|
|
@@ -2421,6 +2516,7 @@ class Puppeteer extends Helper {
|
|
|
2421
2516
|
*/
|
|
2422
2517
|
async waitInUrl(urlPart, sec = null) {
|
|
2423
2518
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2519
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2424
2520
|
|
|
2425
2521
|
return this.page
|
|
2426
2522
|
.waitForFunction(
|
|
@@ -2429,12 +2525,12 @@ class Puppeteer extends Helper {
|
|
|
2429
2525
|
return currUrl.indexOf(urlPart) > -1
|
|
2430
2526
|
},
|
|
2431
2527
|
{ timeout: waitTimeout },
|
|
2432
|
-
|
|
2528
|
+
expectedUrl,
|
|
2433
2529
|
)
|
|
2434
2530
|
.catch(async e => {
|
|
2435
|
-
const currUrl = await this._getPageUrl()
|
|
2531
|
+
const currUrl = await this._getPageUrl()
|
|
2436
2532
|
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2437
|
-
throw new Error(`expected url to include ${
|
|
2533
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2438
2534
|
} else {
|
|
2439
2535
|
throw e
|
|
2440
2536
|
}
|
|
@@ -2446,18 +2542,13 @@ class Puppeteer extends Helper {
|
|
|
2446
2542
|
*/
|
|
2447
2543
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2448
2544
|
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
|
-
}
|
|
2545
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2455
2546
|
|
|
2456
2547
|
return this.page
|
|
2457
2548
|
.waitForFunction(
|
|
2458
2549
|
url => {
|
|
2459
2550
|
const currUrl = decodeURIComponent(window.location.href)
|
|
2460
|
-
return currUrl
|
|
2551
|
+
return currUrl === url
|
|
2461
2552
|
},
|
|
2462
2553
|
{ timeout: waitTimeout },
|
|
2463
2554
|
expectedUrl,
|
|
@@ -2465,11 +2556,36 @@ class Puppeteer extends Helper {
|
|
|
2465
2556
|
.catch(async e => {
|
|
2466
2557
|
const currUrl = await this._getPageUrl()
|
|
2467
2558
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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)
|
|
2572
|
+
|
|
2573
|
+
return this.page
|
|
2574
|
+
.waitForFunction(
|
|
2575
|
+
expectedPath => {
|
|
2576
|
+
const actualPath = window.location.pathname
|
|
2577
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
2578
|
+
return normalizePath(actualPath) === expectedPath
|
|
2579
|
+
},
|
|
2580
|
+
{ timeout: waitTimeout },
|
|
2581
|
+
normalizedPath,
|
|
2582
|
+
)
|
|
2583
|
+
.catch(async e => {
|
|
2584
|
+
const currUrl = await this._getPageUrl()
|
|
2585
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2586
|
+
const actualPath = new URL(currUrl, baseUrl).pathname
|
|
2587
|
+
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2588
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2473
2589
|
} else {
|
|
2474
2590
|
throw e
|
|
2475
2591
|
}
|
|
@@ -3006,10 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3006
3122
|
} else {
|
|
3007
3123
|
assertElementExists(els, locator, 'Clickable element')
|
|
3008
3124
|
}
|
|
3125
|
+
const el = selectElement(els, locator, this)
|
|
3009
3126
|
|
|
3010
|
-
highlightActiveElement.call(this,
|
|
3127
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
3011
3128
|
|
|
3012
|
-
await
|
|
3129
|
+
await el.click(options)
|
|
3013
3130
|
const promises = []
|
|
3014
3131
|
if (options.waitForNavigation) {
|
|
3015
3132
|
promises.push(this.waitForNavigation())
|
|
@@ -3140,43 +3257,57 @@ async function proceedIsChecked(assertType, option) {
|
|
|
3140
3257
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
3141
3258
|
}
|
|
3142
3259
|
|
|
3143
|
-
async function findVisibleFields(locator) {
|
|
3144
|
-
const els = await findFields.call(this, locator)
|
|
3260
|
+
async function findVisibleFields(locator, context = null) {
|
|
3261
|
+
const els = await findFields.call(this, locator, context)
|
|
3145
3262
|
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
3146
3263
|
return els.filter((el, index) => visible[index])
|
|
3147
3264
|
}
|
|
3148
3265
|
|
|
3149
|
-
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
|
+
|
|
3150
3279
|
const matchedLocator = new Locator(locator)
|
|
3151
3280
|
if (!matchedLocator.isFuzzy()) {
|
|
3152
|
-
return
|
|
3281
|
+
return locateFn(matchedLocator)
|
|
3153
3282
|
}
|
|
3154
3283
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3155
3284
|
|
|
3156
|
-
let els = await
|
|
3285
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
3157
3286
|
if (els.length) {
|
|
3158
3287
|
return els
|
|
3159
3288
|
}
|
|
3160
3289
|
|
|
3161
|
-
els = await
|
|
3290
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
3162
3291
|
if (els.length) {
|
|
3163
3292
|
return els
|
|
3164
3293
|
}
|
|
3165
|
-
els = await
|
|
3294
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
3166
3295
|
if (els.length) {
|
|
3167
3296
|
return els
|
|
3168
3297
|
}
|
|
3169
3298
|
|
|
3170
3299
|
// Try ARIA selector for accessible name
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
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
|
+
}
|
|
3177
3308
|
}
|
|
3178
3309
|
|
|
3179
|
-
return
|
|
3310
|
+
return locateFn({ css: matchedLocator.value })
|
|
3180
3311
|
}
|
|
3181
3312
|
|
|
3182
3313
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
@@ -3205,8 +3336,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
|
3205
3336
|
await this._waitForAction()
|
|
3206
3337
|
}
|
|
3207
3338
|
|
|
3208
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
3209
|
-
const els = await findVisibleFields.call(this, field)
|
|
3339
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
3340
|
+
const els = await findVisibleFields.call(this, field, context)
|
|
3210
3341
|
assertElementExists(els, field, 'Field')
|
|
3211
3342
|
const el = els[0]
|
|
3212
3343
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
@@ -3319,6 +3450,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3319
3450
|
}
|
|
3320
3451
|
}
|
|
3321
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
|
+
|
|
3322
3460
|
function $XPath(element, selector) {
|
|
3323
3461
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
3324
3462
|
const res = []
|