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/WebDriver.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
let webdriverio
|
|
2
2
|
|
|
3
|
+
import fs from 'fs'
|
|
3
4
|
import assert from 'assert'
|
|
4
5
|
import path from 'path'
|
|
5
6
|
import crypto from 'crypto'
|
|
@@ -13,17 +14,33 @@ import output from '../output.js'
|
|
|
13
14
|
const { debug } = output
|
|
14
15
|
import { empty } from '../assert/empty.js'
|
|
15
16
|
import { truth } from '../assert/truth.js'
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
xpathLocator,
|
|
19
|
+
fileExists,
|
|
20
|
+
decodeUrl,
|
|
21
|
+
chunkArray,
|
|
22
|
+
convertCssPropertiesToCamelCase,
|
|
23
|
+
screenshotOutputFolder,
|
|
24
|
+
getNormalizedKeyAttributeValue,
|
|
25
|
+
modifierKeys,
|
|
26
|
+
normalizePath,
|
|
27
|
+
resolveUrl,
|
|
28
|
+
getMimeType,
|
|
29
|
+
base64EncodeFile,
|
|
30
|
+
} from '../utils.js'
|
|
17
31
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
18
32
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
33
|
+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
19
34
|
import ConnectionRefused from './errors/ConnectionRefused.js'
|
|
20
35
|
import Locator from '../locator.js'
|
|
21
36
|
import { highlightElement } from './scripts/highlightElement.js'
|
|
22
37
|
import { focusElement } from './scripts/focusElement.js'
|
|
23
38
|
import { blurElement } from './scripts/blurElement.js'
|
|
24
39
|
import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
|
|
40
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
25
41
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
26
42
|
import WebElement from '../element/WebElement.js'
|
|
43
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
27
44
|
|
|
28
45
|
const SHADOW = 'shadow'
|
|
29
46
|
const webRoot = 'body'
|
|
@@ -489,6 +506,7 @@ class WebDriver extends Helper {
|
|
|
489
506
|
keepBrowserState: false,
|
|
490
507
|
deprecationWarnings: false,
|
|
491
508
|
highlightElement: false,
|
|
509
|
+
strict: false,
|
|
492
510
|
}
|
|
493
511
|
|
|
494
512
|
// override defaults with config
|
|
@@ -1076,7 +1094,7 @@ class WebDriver extends Helper {
|
|
|
1076
1094
|
} else {
|
|
1077
1095
|
assertElementExists(res, locator, 'Clickable element')
|
|
1078
1096
|
}
|
|
1079
|
-
const elem =
|
|
1097
|
+
const elem = selectElement(res, locator, this)
|
|
1080
1098
|
highlightActiveElement.call(this, elem)
|
|
1081
1099
|
return this.browser[clickMethod](getElementId(elem))
|
|
1082
1100
|
}
|
|
@@ -1095,7 +1113,7 @@ class WebDriver extends Helper {
|
|
|
1095
1113
|
} else {
|
|
1096
1114
|
assertElementExists(res, locator, 'Clickable element')
|
|
1097
1115
|
}
|
|
1098
|
-
const elem =
|
|
1116
|
+
const elem = selectElement(res, locator, this)
|
|
1099
1117
|
highlightActiveElement.call(this, elem)
|
|
1100
1118
|
|
|
1101
1119
|
return this.executeScript(el => {
|
|
@@ -1123,7 +1141,7 @@ class WebDriver extends Helper {
|
|
|
1123
1141
|
assertElementExists(res, locator, 'Clickable element')
|
|
1124
1142
|
}
|
|
1125
1143
|
|
|
1126
|
-
const elem =
|
|
1144
|
+
const elem = selectElement(res, locator, this)
|
|
1127
1145
|
highlightActiveElement.call(this, elem)
|
|
1128
1146
|
return elem.doubleClick()
|
|
1129
1147
|
}
|
|
@@ -1143,7 +1161,7 @@ class WebDriver extends Helper {
|
|
|
1143
1161
|
assertElementExists(res, locator, 'Clickable element')
|
|
1144
1162
|
}
|
|
1145
1163
|
|
|
1146
|
-
const el =
|
|
1164
|
+
const el = selectElement(res, locator, this)
|
|
1147
1165
|
|
|
1148
1166
|
await el.moveTo()
|
|
1149
1167
|
|
|
@@ -1255,10 +1273,10 @@ class WebDriver extends Helper {
|
|
|
1255
1273
|
* {{ custom }}
|
|
1256
1274
|
*
|
|
1257
1275
|
*/
|
|
1258
|
-
async fillField(field, value) {
|
|
1259
|
-
const res = await findFields.call(this, field)
|
|
1276
|
+
async fillField(field, value, context = null) {
|
|
1277
|
+
const res = await findFields.call(this, field, context)
|
|
1260
1278
|
assertElementExists(res, field, 'Field')
|
|
1261
|
-
const elem =
|
|
1279
|
+
const elem = selectElement(res, field, this)
|
|
1262
1280
|
highlightActiveElement.call(this, elem)
|
|
1263
1281
|
try {
|
|
1264
1282
|
await elem.clearValue()
|
|
@@ -1278,10 +1296,10 @@ class WebDriver extends Helper {
|
|
|
1278
1296
|
* {{> appendField }}
|
|
1279
1297
|
* {{ react }}
|
|
1280
1298
|
*/
|
|
1281
|
-
async appendField(field, value) {
|
|
1282
|
-
const res = await findFields.call(this, field)
|
|
1299
|
+
async appendField(field, value, context = null) {
|
|
1300
|
+
const res = await findFields.call(this, field, context)
|
|
1283
1301
|
assertElementExists(res, field, 'Field')
|
|
1284
|
-
const elem =
|
|
1302
|
+
const elem = selectElement(res, field, this)
|
|
1285
1303
|
highlightActiveElement.call(this, elem)
|
|
1286
1304
|
return elem.addValue(value.toString())
|
|
1287
1305
|
}
|
|
@@ -1290,10 +1308,10 @@ class WebDriver extends Helper {
|
|
|
1290
1308
|
* {{> clearField }}
|
|
1291
1309
|
*
|
|
1292
1310
|
*/
|
|
1293
|
-
async clearField(field) {
|
|
1294
|
-
const res = await findFields.call(this, field)
|
|
1311
|
+
async clearField(field, context = null) {
|
|
1312
|
+
const res = await findFields.call(this, field, context)
|
|
1295
1313
|
assertElementExists(res, field, 'Field')
|
|
1296
|
-
const elem =
|
|
1314
|
+
const elem = selectElement(res, field, this)
|
|
1297
1315
|
highlightActiveElement.call(this, elem)
|
|
1298
1316
|
return elem.clearValue(getElementId(elem))
|
|
1299
1317
|
}
|
|
@@ -1301,30 +1319,31 @@ class WebDriver extends Helper {
|
|
|
1301
1319
|
/**
|
|
1302
1320
|
* {{> selectOption }}
|
|
1303
1321
|
*/
|
|
1304
|
-
async selectOption(select, option) {
|
|
1322
|
+
async selectOption(select, option, context = null) {
|
|
1323
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1305
1324
|
const matchedLocator = new Locator(select)
|
|
1306
1325
|
|
|
1307
1326
|
// Strict locator
|
|
1308
1327
|
if (!matchedLocator.isFuzzy()) {
|
|
1309
1328
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1310
|
-
const els = await
|
|
1329
|
+
const els = await locateFn(select)
|
|
1311
1330
|
assertElementExists(els, select, 'Selectable element')
|
|
1312
|
-
return proceedSelectOption.call(this,
|
|
1331
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1313
1332
|
}
|
|
1314
1333
|
|
|
1315
1334
|
// Fuzzy: try combobox
|
|
1316
1335
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1317
1336
|
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1318
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1337
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1319
1338
|
|
|
1320
1339
|
// Fuzzy: try listbox
|
|
1321
1340
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1322
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1341
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1323
1342
|
|
|
1324
1343
|
// Fuzzy: try native select
|
|
1325
|
-
const res = await findFields.call(this, select)
|
|
1344
|
+
const res = await findFields.call(this, select, context)
|
|
1326
1345
|
assertElementExists(res, select, 'Selectable field')
|
|
1327
|
-
return proceedSelectOption.call(this,
|
|
1346
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1328
1347
|
}
|
|
1329
1348
|
|
|
1330
1349
|
/**
|
|
@@ -1332,28 +1351,41 @@ class WebDriver extends Helper {
|
|
|
1332
1351
|
*
|
|
1333
1352
|
* {{> attachFile }}
|
|
1334
1353
|
*/
|
|
1335
|
-
async attachFile(locator, pathToFile) {
|
|
1354
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1336
1355
|
let file = path.join(global.codecept_dir, pathToFile)
|
|
1337
1356
|
if (!fileExists(file)) {
|
|
1338
1357
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1339
1358
|
}
|
|
1340
1359
|
|
|
1341
|
-
const res = await findFields.call(this, locator)
|
|
1360
|
+
const res = await findFields.call(this, locator, context)
|
|
1342
1361
|
this.debug(`Uploading ${file}`)
|
|
1343
|
-
assertElementExists(res, locator, 'File field')
|
|
1344
|
-
const el = usingFirstElement(res)
|
|
1345
1362
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1363
|
+
if (res.length) {
|
|
1364
|
+
const el = selectElement(res, locator, this)
|
|
1365
|
+
const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
|
|
1366
|
+
const type = await this.browser.execute(function (elem) { return elem.type }, el)
|
|
1367
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
1368
|
+
if (this.options.remoteFileUpload) {
|
|
1369
|
+
try {
|
|
1370
|
+
this.debugSection('File', 'Uploading file to remote server')
|
|
1371
|
+
file = await this.browser.uploadFile(file)
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return el.addValue(file)
|
|
1353
1377
|
}
|
|
1354
1378
|
}
|
|
1355
1379
|
|
|
1356
|
-
|
|
1380
|
+
const targetRes = res.length ? res : await this._locate(locator)
|
|
1381
|
+
assertElementExists(targetRes, locator, 'Element')
|
|
1382
|
+
const targetEl = selectElement(targetRes, locator, this)
|
|
1383
|
+
const fileData = {
|
|
1384
|
+
base64Content: base64EncodeFile(file),
|
|
1385
|
+
fileName: path.basename(file),
|
|
1386
|
+
mimeType: getMimeType(path.basename(file)),
|
|
1387
|
+
}
|
|
1388
|
+
return this.browser.execute(dropFile, targetEl, fileData)
|
|
1357
1389
|
}
|
|
1358
1390
|
|
|
1359
1391
|
/**
|
|
@@ -1367,7 +1399,7 @@ class WebDriver extends Helper {
|
|
|
1367
1399
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1368
1400
|
|
|
1369
1401
|
assertElementExists(res, field, 'Checkable')
|
|
1370
|
-
const elem =
|
|
1402
|
+
const elem = selectElement(res, field, this)
|
|
1371
1403
|
const elementId = getElementId(elem)
|
|
1372
1404
|
highlightActiveElement.call(this, elem)
|
|
1373
1405
|
|
|
@@ -1388,7 +1420,7 @@ class WebDriver extends Helper {
|
|
|
1388
1420
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1389
1421
|
|
|
1390
1422
|
assertElementExists(res, field, 'Checkable')
|
|
1391
|
-
const elem =
|
|
1423
|
+
const elem = selectElement(res, field, this)
|
|
1392
1424
|
const elementId = getElementId(elem)
|
|
1393
1425
|
highlightActiveElement.call(this, elem)
|
|
1394
1426
|
|
|
@@ -1586,18 +1618,18 @@ class WebDriver extends Helper {
|
|
|
1586
1618
|
* {{> seeInField }}
|
|
1587
1619
|
*
|
|
1588
1620
|
*/
|
|
1589
|
-
async seeInField(field, value) {
|
|
1621
|
+
async seeInField(field, value, context = null) {
|
|
1590
1622
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1591
|
-
return proceedSeeField.call(this, 'assert', field, _value)
|
|
1623
|
+
return proceedSeeField.call(this, 'assert', field, _value, context)
|
|
1592
1624
|
}
|
|
1593
1625
|
|
|
1594
1626
|
/**
|
|
1595
1627
|
* {{> dontSeeInField }}
|
|
1596
1628
|
*
|
|
1597
1629
|
*/
|
|
1598
|
-
async dontSeeInField(field, value) {
|
|
1630
|
+
async dontSeeInField(field, value, context = null) {
|
|
1599
1631
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1600
|
-
return proceedSeeField.call(this, 'negate', field, _value)
|
|
1632
|
+
return proceedSeeField.call(this, 'negate', field, _value, context)
|
|
1601
1633
|
}
|
|
1602
1634
|
|
|
1603
1635
|
/**
|
|
@@ -1621,8 +1653,9 @@ class WebDriver extends Helper {
|
|
|
1621
1653
|
* {{ react }}
|
|
1622
1654
|
*
|
|
1623
1655
|
*/
|
|
1624
|
-
async seeElement(locator) {
|
|
1625
|
-
const
|
|
1656
|
+
async seeElement(locator, context = null) {
|
|
1657
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1658
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, true)
|
|
1626
1659
|
assertElementExists(res, locator)
|
|
1627
1660
|
const selected = await forEachAsync(res, async el => el.isDisplayed())
|
|
1628
1661
|
try {
|
|
@@ -1636,8 +1669,9 @@ class WebDriver extends Helper {
|
|
|
1636
1669
|
* {{> dontSeeElement }}
|
|
1637
1670
|
* {{ react }}
|
|
1638
1671
|
*/
|
|
1639
|
-
async dontSeeElement(locator) {
|
|
1640
|
-
const
|
|
1672
|
+
async dontSeeElement(locator, context = null) {
|
|
1673
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1674
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, false)
|
|
1641
1675
|
if (!res || res.length === 0) {
|
|
1642
1676
|
return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
|
|
1643
1677
|
}
|
|
@@ -1844,6 +1878,26 @@ class WebDriver extends Helper {
|
|
|
1844
1878
|
return urlEquals(this.options.url).negate(url, decodeUrl(res))
|
|
1845
1879
|
}
|
|
1846
1880
|
|
|
1881
|
+
/**
|
|
1882
|
+
* {{> seeCurrentPathEquals }}
|
|
1883
|
+
*/
|
|
1884
|
+
async seeCurrentPathEquals(path) {
|
|
1885
|
+
const currentUrl = await this.browser.getUrl()
|
|
1886
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1887
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1888
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
1893
|
+
*/
|
|
1894
|
+
async dontSeeCurrentPathEquals(path) {
|
|
1895
|
+
const currentUrl = await this.browser.getUrl()
|
|
1896
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1897
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1898
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1847
1901
|
/**
|
|
1848
1902
|
* Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
|
|
1849
1903
|
*
|
|
@@ -1916,8 +1970,22 @@ class WebDriver extends Helper {
|
|
|
1916
1970
|
* {{> moveCursorTo }}
|
|
1917
1971
|
*/
|
|
1918
1972
|
async moveCursorTo(locator, xOffset, yOffset) {
|
|
1919
|
-
|
|
1920
|
-
|
|
1973
|
+
let context = null
|
|
1974
|
+
if (typeof xOffset !== 'number' && xOffset !== undefined) {
|
|
1975
|
+
context = xOffset
|
|
1976
|
+
xOffset = undefined
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
let res
|
|
1980
|
+
if (context) {
|
|
1981
|
+
const contextRes = await this._locate(withStrictLocator(context), true)
|
|
1982
|
+
assertElementExists(contextRes, context, 'Context element')
|
|
1983
|
+
res = await contextRes[0].$$(withStrictLocator(locator))
|
|
1984
|
+
assertElementExists(res, locator)
|
|
1985
|
+
} else {
|
|
1986
|
+
res = await this._locate(withStrictLocator(locator), true)
|
|
1987
|
+
assertElementExists(res, locator)
|
|
1988
|
+
}
|
|
1921
1989
|
const elem = usingFirstElement(res)
|
|
1922
1990
|
try {
|
|
1923
1991
|
await elem.moveTo({ xOffset, yOffset })
|
|
@@ -2467,6 +2535,7 @@ class WebDriver extends Helper {
|
|
|
2467
2535
|
async waitInUrl(urlPart, sec = null) {
|
|
2468
2536
|
const client = this.browser
|
|
2469
2537
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2538
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2470
2539
|
let currUrl = ''
|
|
2471
2540
|
|
|
2472
2541
|
return client
|
|
@@ -2474,7 +2543,7 @@ class WebDriver extends Helper {
|
|
|
2474
2543
|
function () {
|
|
2475
2544
|
return this.getUrl().then(res => {
|
|
2476
2545
|
currUrl = decodeUrl(res)
|
|
2477
|
-
return currUrl.indexOf(
|
|
2546
|
+
return currUrl.indexOf(expectedUrl) > -1
|
|
2478
2547
|
})
|
|
2479
2548
|
},
|
|
2480
2549
|
{ timeout: aSec * 1000 },
|
|
@@ -2482,7 +2551,7 @@ class WebDriver extends Helper {
|
|
|
2482
2551
|
.catch(e => {
|
|
2483
2552
|
e = wrapError(e)
|
|
2484
2553
|
if (e.message.indexOf('timeout')) {
|
|
2485
|
-
throw new Error(`expected url to include ${
|
|
2554
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2486
2555
|
}
|
|
2487
2556
|
throw e
|
|
2488
2557
|
})
|
|
@@ -2493,22 +2562,47 @@ class WebDriver extends Helper {
|
|
|
2493
2562
|
*/
|
|
2494
2563
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2495
2564
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2496
|
-
const
|
|
2497
|
-
if (urlPart.indexOf('http') < 0) {
|
|
2498
|
-
urlPart = baseUrl + urlPart
|
|
2499
|
-
}
|
|
2565
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2500
2566
|
let currUrl = ''
|
|
2501
2567
|
return this.browser
|
|
2502
2568
|
.waitUntil(function () {
|
|
2503
2569
|
return this.getUrl().then(res => {
|
|
2504
2570
|
currUrl = decodeUrl(res)
|
|
2505
|
-
return currUrl ===
|
|
2571
|
+
return currUrl === expectedUrl
|
|
2506
2572
|
})
|
|
2507
2573
|
}, aSec * 1000)
|
|
2508
2574
|
.catch(e => {
|
|
2509
2575
|
e = wrapError(e)
|
|
2510
2576
|
if (e.message.indexOf('timeout')) {
|
|
2511
|
-
throw new Error(`expected url to be ${
|
|
2577
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
2578
|
+
}
|
|
2579
|
+
throw e
|
|
2580
|
+
})
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* {{> waitCurrentPathEquals }}
|
|
2585
|
+
*/
|
|
2586
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
2587
|
+
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2588
|
+
const normalizedPath = normalizePath(path)
|
|
2589
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2590
|
+
let actualPath = ''
|
|
2591
|
+
|
|
2592
|
+
return this.browser
|
|
2593
|
+
.waitUntil(
|
|
2594
|
+
async () => {
|
|
2595
|
+
const currUrl = await this.browser.getUrl()
|
|
2596
|
+
const url = new URL(currUrl, baseUrl)
|
|
2597
|
+
actualPath = url.pathname
|
|
2598
|
+
return normalizePath(actualPath) === normalizedPath
|
|
2599
|
+
},
|
|
2600
|
+
{ timeout: aSec * 1000 },
|
|
2601
|
+
)
|
|
2602
|
+
.catch(e => {
|
|
2603
|
+
e = wrapError(e)
|
|
2604
|
+
if (e.message.indexOf('timeout')) {
|
|
2605
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2512
2606
|
}
|
|
2513
2607
|
throw e
|
|
2514
2608
|
})
|
|
@@ -2990,32 +3084,33 @@ async function findClickable(locator, locateFn) {
|
|
|
2990
3084
|
return await locateFn(locator.value) // by css or xpath
|
|
2991
3085
|
}
|
|
2992
3086
|
|
|
2993
|
-
async function findFields(locator) {
|
|
3087
|
+
async function findFields(locator, context = null) {
|
|
3088
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
2994
3089
|
locator = new Locator(locator)
|
|
2995
3090
|
|
|
2996
3091
|
if (this._isCustomLocator(locator)) {
|
|
2997
|
-
return
|
|
3092
|
+
return locateFn(locator)
|
|
2998
3093
|
}
|
|
2999
3094
|
|
|
3000
|
-
if (locator.isAccessibilityId() && !this.isWeb) return
|
|
3001
|
-
if (locator.isRole()) return
|
|
3002
|
-
if (!locator.isFuzzy()) return
|
|
3095
|
+
if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator)
|
|
3096
|
+
if (locator.isRole()) return locateFn(locator)
|
|
3097
|
+
if (!locator.isFuzzy()) return locateFn(locator)
|
|
3003
3098
|
|
|
3004
3099
|
const literal = xpathLocator.literal(locator.value)
|
|
3005
|
-
let els = await
|
|
3100
|
+
let els = await locateFn(Locator.field.labelEquals(literal))
|
|
3006
3101
|
if (els.length) return els
|
|
3007
3102
|
|
|
3008
|
-
els = await
|
|
3103
|
+
els = await locateFn(Locator.field.labelContains(literal))
|
|
3009
3104
|
if (els.length) return els
|
|
3010
3105
|
|
|
3011
|
-
els = await
|
|
3106
|
+
els = await locateFn(Locator.field.byName(literal))
|
|
3012
3107
|
if (els.length) return els
|
|
3013
3108
|
|
|
3014
|
-
return await
|
|
3109
|
+
return await locateFn(locator.value) // by css or xpath
|
|
3015
3110
|
}
|
|
3016
3111
|
|
|
3017
|
-
async function proceedSeeField(assertType, field, value) {
|
|
3018
|
-
const res = await findFields.call(this, field)
|
|
3112
|
+
async function proceedSeeField(assertType, field, value, context) {
|
|
3113
|
+
const res = await findFields.call(this, field, context)
|
|
3019
3114
|
assertElementExists(res, field, 'Field')
|
|
3020
3115
|
const elem = usingFirstElement(res)
|
|
3021
3116
|
const elemId = getElementId(elem)
|
|
@@ -3200,10 +3295,30 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3200
3295
|
}
|
|
3201
3296
|
|
|
3202
3297
|
function usingFirstElement(els) {
|
|
3298
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
3299
|
+
if (rawIndex != null && els.length > 1) {
|
|
3300
|
+
let elementIndex = rawIndex
|
|
3301
|
+
if (elementIndex === 'first') elementIndex = 1
|
|
3302
|
+
if (elementIndex === 'last') elementIndex = -1
|
|
3303
|
+
if (Number.isInteger(elementIndex) && elementIndex !== 0) {
|
|
3304
|
+
const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
|
|
3305
|
+
if (idx >= 0 && idx < els.length) {
|
|
3306
|
+
debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
|
|
3307
|
+
return els[idx]
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3203
3311
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3204
3312
|
return els[0]
|
|
3205
3313
|
}
|
|
3206
3314
|
|
|
3315
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
3316
|
+
if (elements.length > 1) {
|
|
3317
|
+
const webElements = Array.from(elements).map(el => new WebElement(el, helper))
|
|
3318
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3207
3322
|
function getElementId(el) {
|
|
3208
3323
|
// W3C WebDriver web element identifier
|
|
3209
3324
|
// https://w3c.github.io/webdriver/#dfn-web-element-identifier
|
|
@@ -1,40 +1,45 @@
|
|
|
1
1
|
import Locator from '../../locator.js'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Error thrown when strict mode is enabled and multiple elements are found
|
|
5
|
-
* for a single-element locator operation (click, fillField, etc.)
|
|
6
|
-
*/
|
|
7
3
|
class MultipleElementsFound extends Error {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
super(`Multiple elements (${elements.length}) found for "${locator}". Call fetchDetails() for full information.`)
|
|
4
|
+
constructor(locator, webElements) {
|
|
5
|
+
const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
|
|
6
|
+
? new Locator(locator).toString()
|
|
7
|
+
: String(locator)
|
|
8
|
+
super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
|
|
14
9
|
this.name = 'MultipleElementsFound'
|
|
15
10
|
this.locator = locator
|
|
16
|
-
this.
|
|
17
|
-
this.count =
|
|
11
|
+
this.webElements = webElements
|
|
12
|
+
this.count = webElements.length
|
|
18
13
|
this._detailsFetched = false
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
/**
|
|
22
|
-
* Fetch detailed information about the found elements asynchronously
|
|
23
|
-
* This updates the error message with XPath and element previews
|
|
24
|
-
*/
|
|
25
16
|
async fetchDetails() {
|
|
26
17
|
if (this._detailsFetched) return
|
|
27
18
|
|
|
28
19
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const items = []
|
|
21
|
+
const maxToShow = Math.min(this.count, 10)
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < maxToShow; i++) {
|
|
24
|
+
const webEl = this.webElements[i]
|
|
25
|
+
try {
|
|
26
|
+
const xpath = await webEl.toAbsoluteXPath()
|
|
27
|
+
const html = await webEl.toSimplifiedHTML()
|
|
28
|
+
items.push(` ${i + 1}. > ${xpath}\n ${html}`)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
|
|
31
|
+
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if (this.count > 10) {
|
|
35
|
+
items.push(` ... and ${this.count - 10} more`)
|
|
36
|
+
}
|
|
35
37
|
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
+
const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
|
|
39
|
+
? new Locator(this.locator).toString()
|
|
40
|
+
: String(this.locator)
|
|
41
|
+
this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
|
|
42
|
+
items.join('\n') +
|
|
38
43
|
`\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
|
|
39
44
|
} catch (err) {
|
|
40
45
|
this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
|
|
@@ -42,94 +47,6 @@ class MultipleElementsFound extends Error {
|
|
|
42
47
|
|
|
43
48
|
this._detailsFetched = true
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Generate a formatted list of found elements with their XPath and preview
|
|
48
|
-
* @param {Array<HTMLElement>} elements
|
|
49
|
-
* @param {number} count
|
|
50
|
-
* @returns {Promise<string>}
|
|
51
|
-
*/
|
|
52
|
-
async _generateElementList(elements, count) {
|
|
53
|
-
const items = []
|
|
54
|
-
const maxToShow = Math.min(count, 10)
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < maxToShow; i++) {
|
|
57
|
-
const el = elements[i]
|
|
58
|
-
try {
|
|
59
|
-
const info = await this._getElementInfo(el)
|
|
60
|
-
items.push(` ${i + 1}. ${info.xpath} (${info.preview})`)
|
|
61
|
-
} catch (err) {
|
|
62
|
-
// Element might be detached or inaccessible
|
|
63
|
-
items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (count > 10) {
|
|
68
|
-
items.push(` ... and ${count - 10} more`)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return items.join('\n')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get XPath and preview for an element by running JavaScript in browser context
|
|
76
|
-
* @param {HTMLElement} element
|
|
77
|
-
* @returns {Promise<{xpath: string, preview: string}>}
|
|
78
|
-
*/
|
|
79
|
-
async _getElementInfo(element) {
|
|
80
|
-
return element.evaluate((el) => {
|
|
81
|
-
// Generate a unique XPath for this element
|
|
82
|
-
const getUniqueXPath = (element) => {
|
|
83
|
-
if (element.id) {
|
|
84
|
-
return `//*[@id="${element.id}"]`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const parts = []
|
|
88
|
-
let current = element
|
|
89
|
-
|
|
90
|
-
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
91
|
-
let index = 0
|
|
92
|
-
let sibling = current.previousSibling
|
|
93
|
-
|
|
94
|
-
while (sibling) {
|
|
95
|
-
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
|
|
96
|
-
index++
|
|
97
|
-
}
|
|
98
|
-
sibling = sibling.previousSibling
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const tagName = current.tagName.toLowerCase()
|
|
102
|
-
const pathIndex = index > 0 ? `[${index + 1}]` : ''
|
|
103
|
-
parts.unshift(`${tagName}${pathIndex}`)
|
|
104
|
-
|
|
105
|
-
current = current.parentElement
|
|
106
|
-
|
|
107
|
-
// Stop at body to keep XPath reasonable
|
|
108
|
-
if (current && current.tagName === 'BODY') {
|
|
109
|
-
parts.unshift('body')
|
|
110
|
-
break
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return '/' + parts.join('/')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Get a preview of the element (tag, classes, id)
|
|
118
|
-
const getPreview = (element) => {
|
|
119
|
-
const tag = element.tagName.toLowerCase()
|
|
120
|
-
const id = element.id ? `#${element.id}` : ''
|
|
121
|
-
const classes = element.className
|
|
122
|
-
? '.' + element.className.split(' ').filter(c => c).join('.')
|
|
123
|
-
: ''
|
|
124
|
-
return `${tag}${id}${classes || ''}`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
xpath: getUniqueXPath(el),
|
|
129
|
-
preview: getPreview(el),
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
50
|
}
|
|
134
51
|
|
|
135
52
|
export default MultipleElementsFound
|