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.
- package/README.md +39 -27
- package/bin/mcp-server.js +637 -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 +224 -138
- package/lib/helper/Puppeteer.js +211 -69
- package/lib/helper/WebDriver.js +183 -64
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -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
|
@@ -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
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1578
|
-
await
|
|
1579
|
-
await
|
|
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
|
-
|
|
1612
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
1634
|
-
|
|
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(
|
|
1638
|
-
if (els?.length) return proceedSelect.call(this,
|
|
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,
|
|
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
|
-
|
|
2532
|
+
expectedUrl,
|
|
2433
2533
|
)
|
|
2434
2534
|
.catch(async e => {
|
|
2435
|
-
const currUrl = await this._getPageUrl()
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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,
|
|
3131
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
3011
3132
|
|
|
3012
|
-
await
|
|
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
|
|
3285
|
+
return locateFn(matchedLocator)
|
|
3153
3286
|
}
|
|
3154
3287
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3155
3288
|
|
|
3156
|
-
let els = await
|
|
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
|
|
3294
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
3162
3295
|
if (els.length) {
|
|
3163
3296
|
return els
|
|
3164
3297
|
}
|
|
3165
|
-
els = await
|
|
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
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
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
|
|
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 = []
|