codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -27
- package/bin/codecept.js +2 -2
- 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/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- 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/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- 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 -11011
- package/typings/types.d.ts +0 -13073
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,49 +1651,71 @@ class Puppeteer extends Helper {
|
|
|
1601
1651
|
*
|
|
1602
1652
|
* {{> attachFile }}
|
|
1603
1653
|
*/
|
|
1604
|
-
async attachFile(locator, pathToFile) {
|
|
1654
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1605
1655
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
1606
1656
|
|
|
1607
1657
|
if (!fileExists(file)) {
|
|
1608
1658
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1609
1659
|
}
|
|
1610
|
-
const els = await findFields.call(this, locator)
|
|
1611
|
-
|
|
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
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
for (const key in option) {
|
|
1630
|
-
const opt = xpathLocator.literal(option[key])
|
|
1631
|
-
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
|
|
1632
|
-
if (optEl.length) {
|
|
1633
|
-
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1634
|
-
continue
|
|
1635
|
-
}
|
|
1636
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
|
|
1637
|
-
if (optEl.length) {
|
|
1638
|
-
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1639
|
-
}
|
|
1686
|
+
async selectOption(select, option, context = null) {
|
|
1687
|
+
const pageContext = await this._getContext()
|
|
1688
|
+
const matchedLocator = new Locator(select)
|
|
1689
|
+
|
|
1690
|
+
let contextEl
|
|
1691
|
+
if (context) {
|
|
1692
|
+
const contextEls = await findElements.call(this, pageContext, context)
|
|
1693
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1694
|
+
contextEl = contextEls[0]
|
|
1640
1695
|
}
|
|
1641
|
-
await this._evaluateHandeInContext(element => {
|
|
1642
|
-
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1643
|
-
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1644
|
-
}, el)
|
|
1645
1696
|
|
|
1646
|
-
|
|
1697
|
+
// Strict locator
|
|
1698
|
+
if (!matchedLocator.isFuzzy()) {
|
|
1699
|
+
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1700
|
+
const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
|
|
1701
|
+
assertElementExists(els, select, 'Selectable element')
|
|
1702
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Fuzzy: try combobox
|
|
1706
|
+
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1707
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
1708
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
1709
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1710
|
+
|
|
1711
|
+
// Fuzzy: try listbox
|
|
1712
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
1713
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1714
|
+
|
|
1715
|
+
// Fuzzy: try native select
|
|
1716
|
+
const visibleEls = await findVisibleFields.call(this, select, context)
|
|
1717
|
+
assertElementExists(visibleEls, select, 'Selectable field')
|
|
1718
|
+
return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
|
|
1647
1719
|
}
|
|
1648
1720
|
|
|
1649
1721
|
/**
|
|
@@ -1687,6 +1759,26 @@ class Puppeteer extends Helper {
|
|
|
1687
1759
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
1688
1760
|
}
|
|
1689
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* {{> seeCurrentPathEquals }}
|
|
1764
|
+
*/
|
|
1765
|
+
async seeCurrentPathEquals(path) {
|
|
1766
|
+
const currentUrl = await this._getPageUrl()
|
|
1767
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1768
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1769
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
1774
|
+
*/
|
|
1775
|
+
async dontSeeCurrentPathEquals(path) {
|
|
1776
|
+
const currentUrl = await this._getPageUrl()
|
|
1777
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1778
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1779
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1690
1782
|
/**
|
|
1691
1783
|
* {{> see }}
|
|
1692
1784
|
*
|
|
@@ -2424,6 +2516,7 @@ class Puppeteer extends Helper {
|
|
|
2424
2516
|
*/
|
|
2425
2517
|
async waitInUrl(urlPart, sec = null) {
|
|
2426
2518
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2519
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2427
2520
|
|
|
2428
2521
|
return this.page
|
|
2429
2522
|
.waitForFunction(
|
|
@@ -2432,12 +2525,12 @@ class Puppeteer extends Helper {
|
|
|
2432
2525
|
return currUrl.indexOf(urlPart) > -1
|
|
2433
2526
|
},
|
|
2434
2527
|
{ timeout: waitTimeout },
|
|
2435
|
-
|
|
2528
|
+
expectedUrl,
|
|
2436
2529
|
)
|
|
2437
2530
|
.catch(async e => {
|
|
2438
|
-
const currUrl = await this._getPageUrl()
|
|
2531
|
+
const currUrl = await this._getPageUrl()
|
|
2439
2532
|
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2440
|
-
throw new Error(`expected url to include ${
|
|
2533
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2441
2534
|
} else {
|
|
2442
2535
|
throw e
|
|
2443
2536
|
}
|
|
@@ -2449,25 +2542,50 @@ class Puppeteer extends Helper {
|
|
|
2449
2542
|
*/
|
|
2450
2543
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2451
2544
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2545
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2452
2546
|
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2547
|
+
return this.page
|
|
2548
|
+
.waitForFunction(
|
|
2549
|
+
url => {
|
|
2550
|
+
const currUrl = decodeURIComponent(window.location.href)
|
|
2551
|
+
return currUrl === url
|
|
2552
|
+
},
|
|
2553
|
+
{ timeout: waitTimeout },
|
|
2554
|
+
expectedUrl,
|
|
2555
|
+
)
|
|
2556
|
+
.catch(async e => {
|
|
2557
|
+
const currUrl = await this._getPageUrl()
|
|
2558
|
+
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2559
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
2560
|
+
} else {
|
|
2561
|
+
throw e
|
|
2562
|
+
}
|
|
2563
|
+
})
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* {{> waitCurrentPathEquals }}
|
|
2568
|
+
*/
|
|
2569
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
2570
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2571
|
+
const normalizedPath = normalizePath(path)
|
|
2457
2572
|
|
|
2458
2573
|
return this.page
|
|
2459
2574
|
.waitForFunction(
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2575
|
+
expectedPath => {
|
|
2576
|
+
const actualPath = window.location.pathname
|
|
2577
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
2578
|
+
return normalizePath(actualPath) === expectedPath
|
|
2463
2579
|
},
|
|
2464
2580
|
{ timeout: waitTimeout },
|
|
2465
|
-
|
|
2581
|
+
normalizedPath,
|
|
2466
2582
|
)
|
|
2467
2583
|
.catch(async e => {
|
|
2468
|
-
const currUrl = await this._getPageUrl()
|
|
2584
|
+
const currUrl = await this._getPageUrl()
|
|
2585
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2586
|
+
const actualPath = new URL(currUrl, baseUrl).pathname
|
|
2469
2587
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2470
|
-
throw new Error(`expected
|
|
2588
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2471
2589
|
} else {
|
|
2472
2590
|
throw e
|
|
2473
2591
|
}
|
|
@@ -2895,10 +3013,17 @@ async function findElements(matcher, locator) {
|
|
|
2895
3013
|
if (isReactLocator) return findReactElements.call(this, locator)
|
|
2896
3014
|
|
|
2897
3015
|
locator = new Locator(locator, 'css')
|
|
2898
|
-
|
|
3016
|
+
|
|
2899
3017
|
// Check if locator is a role locator and call findByRole
|
|
2900
3018
|
if (locator.isRole()) return findByRole.call(this, matcher, locator)
|
|
2901
3019
|
|
|
3020
|
+
// Handle shadow DOM locators with >>> deep descendant combinator
|
|
3021
|
+
// { shadow: ['my-app', 'recipe-hello', 'button'] } => 'my-app >>> recipe-hello >>> button'
|
|
3022
|
+
if (locator.isShadow()) {
|
|
3023
|
+
const shadowSelector = locator.value.join(' >>> ')
|
|
3024
|
+
return matcher.$$(shadowSelector)
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2902
3027
|
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
|
|
2903
3028
|
if (!locator.isXPath()) return matcher.$$(locator.simplify())
|
|
2904
3029
|
|
|
@@ -2955,22 +3080,32 @@ async function findElements(matcher, locator) {
|
|
|
2955
3080
|
async function findElement(matcher, locator) {
|
|
2956
3081
|
if (locator.react) return findReactElements.call(this, locator)
|
|
2957
3082
|
locator = new Locator(locator, 'css')
|
|
2958
|
-
|
|
3083
|
+
|
|
2959
3084
|
// Check if locator is a role locator and call findByRole
|
|
2960
3085
|
if (locator.isRole()) {
|
|
2961
3086
|
const elements = await findByRole.call(this, matcher, locator)
|
|
2962
3087
|
return elements[0]
|
|
2963
3088
|
}
|
|
2964
3089
|
|
|
3090
|
+
// Handle shadow DOM locators with >>> deep descendant combinator
|
|
3091
|
+
if (locator.isShadow()) {
|
|
3092
|
+
const shadowSelector = locator.value.join(' >>> ')
|
|
3093
|
+
const elements = await matcher.$$(shadowSelector)
|
|
3094
|
+
return elements[0]
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2965
3097
|
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
|
|
2966
3098
|
if (!locator.isXPath()) {
|
|
2967
3099
|
const elements = await matcher.$$(locator.simplify())
|
|
2968
3100
|
return elements[0]
|
|
2969
3101
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3102
|
+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
3103
|
+
if (puppeteer.default?.defaultBrowserRevision) {
|
|
3104
|
+
const elements = await matcher.$$(`xpath/${locator.value}`)
|
|
3105
|
+
return elements[0]
|
|
3106
|
+
}
|
|
3107
|
+
// For Puppeteer 24.x+, $x method was removed - use ::-p-xpath() selector
|
|
3108
|
+
const elements = await matcher.$$(`::-p-xpath(${locator.value})`)
|
|
2974
3109
|
return elements[0]
|
|
2975
3110
|
}
|
|
2976
3111
|
|
|
@@ -2987,10 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2987
3122
|
} else {
|
|
2988
3123
|
assertElementExists(els, locator, 'Clickable element')
|
|
2989
3124
|
}
|
|
3125
|
+
const el = selectElement(els, locator, this)
|
|
2990
3126
|
|
|
2991
|
-
highlightActiveElement.call(this,
|
|
3127
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
2992
3128
|
|
|
2993
|
-
await
|
|
3129
|
+
await el.click(options)
|
|
2994
3130
|
const promises = []
|
|
2995
3131
|
if (options.waitForNavigation) {
|
|
2996
3132
|
promises.push(this.waitForNavigation())
|
|
@@ -3121,43 +3257,57 @@ async function proceedIsChecked(assertType, option) {
|
|
|
3121
3257
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
3122
3258
|
}
|
|
3123
3259
|
|
|
3124
|
-
async function findVisibleFields(locator) {
|
|
3125
|
-
const els = await findFields.call(this, locator)
|
|
3260
|
+
async function findVisibleFields(locator, context = null) {
|
|
3261
|
+
const els = await findFields.call(this, locator, context)
|
|
3126
3262
|
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
3127
3263
|
return els.filter((el, index) => visible[index])
|
|
3128
3264
|
}
|
|
3129
3265
|
|
|
3130
|
-
async function findFields(locator) {
|
|
3266
|
+
async function findFields(locator, context = null) {
|
|
3267
|
+
let contextEl
|
|
3268
|
+
if (context) {
|
|
3269
|
+
const contextPage = await this.context
|
|
3270
|
+
const contextEls = await findElements.call(this, contextPage, context)
|
|
3271
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
3272
|
+
contextEl = contextEls[0]
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const locateFn = contextEl
|
|
3276
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
3277
|
+
: loc => this._locate(loc)
|
|
3278
|
+
|
|
3131
3279
|
const matchedLocator = new Locator(locator)
|
|
3132
3280
|
if (!matchedLocator.isFuzzy()) {
|
|
3133
|
-
return
|
|
3281
|
+
return locateFn(matchedLocator)
|
|
3134
3282
|
}
|
|
3135
3283
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3136
3284
|
|
|
3137
|
-
let els = await
|
|
3285
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
3138
3286
|
if (els.length) {
|
|
3139
3287
|
return els
|
|
3140
3288
|
}
|
|
3141
3289
|
|
|
3142
|
-
els = await
|
|
3290
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
3143
3291
|
if (els.length) {
|
|
3144
3292
|
return els
|
|
3145
3293
|
}
|
|
3146
|
-
els = await
|
|
3294
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
3147
3295
|
if (els.length) {
|
|
3148
3296
|
return els
|
|
3149
3297
|
}
|
|
3150
3298
|
|
|
3151
3299
|
// Try ARIA selector for accessible name
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3300
|
+
if (!contextEl) {
|
|
3301
|
+
try {
|
|
3302
|
+
const page = await this.context
|
|
3303
|
+
els = await page.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3304
|
+
if (els.length) return els
|
|
3305
|
+
} catch (err) {
|
|
3306
|
+
// ARIA selector not supported or failed
|
|
3307
|
+
}
|
|
3158
3308
|
}
|
|
3159
3309
|
|
|
3160
|
-
return
|
|
3310
|
+
return locateFn({ css: matchedLocator.value })
|
|
3161
3311
|
}
|
|
3162
3312
|
|
|
3163
3313
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
@@ -3186,8 +3336,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
|
3186
3336
|
await this._waitForAction()
|
|
3187
3337
|
}
|
|
3188
3338
|
|
|
3189
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
3190
|
-
const els = await findVisibleFields.call(this, field)
|
|
3339
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
3340
|
+
const els = await findVisibleFields.call(this, field, context)
|
|
3191
3341
|
assertElementExists(els, field, 'Field')
|
|
3192
3342
|
const el = els[0]
|
|
3193
3343
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
@@ -3300,6 +3450,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3300
3450
|
}
|
|
3301
3451
|
}
|
|
3302
3452
|
|
|
3453
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
3454
|
+
if (elements.length > 1) {
|
|
3455
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
3456
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3303
3460
|
function $XPath(element, selector) {
|
|
3304
3461
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
3305
3462
|
const res = []
|
|
@@ -3538,3 +3695,81 @@ function createRoleTextMatcher(expected, exactMatch) {
|
|
|
3538
3695
|
return value => typeof value === 'string' && value.includes(target)
|
|
3539
3696
|
}
|
|
3540
3697
|
|
|
3698
|
+
async function proceedSelect(context, el, option) {
|
|
3699
|
+
const role = await el.evaluate(e => e.getAttribute('role'))
|
|
3700
|
+
const options = Array.isArray(option) ? option : [option]
|
|
3701
|
+
|
|
3702
|
+
if (role === 'combobox') {
|
|
3703
|
+
this.debugSection('SelectOption', 'Expanding combobox')
|
|
3704
|
+
highlightActiveElement.call(this, el, context)
|
|
3705
|
+
const ariaOwns = await el.evaluate(e => e.getAttribute('aria-owns'))
|
|
3706
|
+
const ariaControls = await el.evaluate(e => e.getAttribute('aria-controls'))
|
|
3707
|
+
await el.click()
|
|
3708
|
+
await this._waitForAction()
|
|
3709
|
+
|
|
3710
|
+
const listboxId = ariaOwns || ariaControls
|
|
3711
|
+
let listbox = null
|
|
3712
|
+
if (listboxId) {
|
|
3713
|
+
const listboxEls = await context.$$( `#${listboxId}`)
|
|
3714
|
+
if (listboxEls.length) listbox = listboxEls[0]
|
|
3715
|
+
}
|
|
3716
|
+
if (!listbox) {
|
|
3717
|
+
const listboxEls = await findByRole.call(this, context, { role: 'listbox' })
|
|
3718
|
+
if (listboxEls?.length) listbox = listboxEls[0]
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (listbox) {
|
|
3722
|
+
for (const opt of options) {
|
|
3723
|
+
const optEls = await findByRole.call(this, listbox, { role: 'option', name: opt })
|
|
3724
|
+
if (optEls?.length) {
|
|
3725
|
+
const optEl = optEls[0]
|
|
3726
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3727
|
+
highlightActiveElement.call(this, optEl, context)
|
|
3728
|
+
await optEl.click()
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
return this._waitForAction()
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
if (role === 'listbox') {
|
|
3736
|
+
for (const opt of options) {
|
|
3737
|
+
const optEls = await findByRole.call(this, el, { role: 'option', name: opt })
|
|
3738
|
+
if (optEls?.length) {
|
|
3739
|
+
const optEl = optEls[0]
|
|
3740
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3741
|
+
highlightActiveElement.call(this, optEl, context)
|
|
3742
|
+
await optEl.click()
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
return this._waitForAction()
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
// Native <select> element
|
|
3749
|
+
const tagName = await el.evaluate(e => e.tagName)
|
|
3750
|
+
if (tagName !== 'SELECT') {
|
|
3751
|
+
throw new Error('Element is not <select>')
|
|
3752
|
+
}
|
|
3753
|
+
highlightActiveElement.call(this, el, context)
|
|
3754
|
+
const optionArray = Array.isArray(option) ? option : [option]
|
|
3755
|
+
|
|
3756
|
+
for (const key in optionArray) {
|
|
3757
|
+
const opt = xpathLocator.literal(optionArray[key])
|
|
3758
|
+
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
|
|
3759
|
+
if (optEl.length) {
|
|
3760
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
3761
|
+
continue
|
|
3762
|
+
}
|
|
3763
|
+
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
|
|
3764
|
+
if (optEl.length) {
|
|
3765
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
await this._evaluateHandeInContext(element => {
|
|
3769
|
+
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3770
|
+
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3771
|
+
}, el)
|
|
3772
|
+
|
|
3773
|
+
return this._waitForAction()
|
|
3774
|
+
}
|
|
3775
|
+
|