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/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'
|
|
@@ -9,21 +10,38 @@ import promiseRetry from 'promise-retry'
|
|
|
9
10
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
10
11
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
11
12
|
import store from '../store.js'
|
|
13
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
12
14
|
import output from '../output.js'
|
|
13
15
|
const { debug } = output
|
|
14
16
|
import { empty } from '../assert/empty.js'
|
|
15
17
|
import { truth } from '../assert/truth.js'
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
xpathLocator,
|
|
20
|
+
fileExists,
|
|
21
|
+
decodeUrl,
|
|
22
|
+
chunkArray,
|
|
23
|
+
convertCssPropertiesToCamelCase,
|
|
24
|
+
screenshotOutputFolder,
|
|
25
|
+
getNormalizedKeyAttributeValue,
|
|
26
|
+
modifierKeys,
|
|
27
|
+
normalizePath,
|
|
28
|
+
resolveUrl,
|
|
29
|
+
getMimeType,
|
|
30
|
+
base64EncodeFile,
|
|
31
|
+
} from '../utils.js'
|
|
17
32
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
18
33
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
34
|
+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
19
35
|
import ConnectionRefused from './errors/ConnectionRefused.js'
|
|
20
36
|
import Locator from '../locator.js'
|
|
21
37
|
import { highlightElement } from './scripts/highlightElement.js'
|
|
22
38
|
import { focusElement } from './scripts/focusElement.js'
|
|
23
39
|
import { blurElement } from './scripts/blurElement.js'
|
|
24
40
|
import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
|
|
41
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
25
42
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
26
43
|
import WebElement from '../element/WebElement.js'
|
|
44
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
27
45
|
|
|
28
46
|
const SHADOW = 'shadow'
|
|
29
47
|
const webRoot = 'body'
|
|
@@ -489,6 +507,7 @@ class WebDriver extends Helper {
|
|
|
489
507
|
keepBrowserState: false,
|
|
490
508
|
deprecationWarnings: false,
|
|
491
509
|
highlightElement: false,
|
|
510
|
+
strict: false,
|
|
492
511
|
}
|
|
493
512
|
|
|
494
513
|
// override defaults with config
|
|
@@ -1076,7 +1095,7 @@ class WebDriver extends Helper {
|
|
|
1076
1095
|
} else {
|
|
1077
1096
|
assertElementExists(res, locator, 'Clickable element')
|
|
1078
1097
|
}
|
|
1079
|
-
const elem =
|
|
1098
|
+
const elem = selectElement(res, locator, this)
|
|
1080
1099
|
highlightActiveElement.call(this, elem)
|
|
1081
1100
|
return this.browser[clickMethod](getElementId(elem))
|
|
1082
1101
|
}
|
|
@@ -1095,7 +1114,7 @@ class WebDriver extends Helper {
|
|
|
1095
1114
|
} else {
|
|
1096
1115
|
assertElementExists(res, locator, 'Clickable element')
|
|
1097
1116
|
}
|
|
1098
|
-
const elem =
|
|
1117
|
+
const elem = selectElement(res, locator, this)
|
|
1099
1118
|
highlightActiveElement.call(this, elem)
|
|
1100
1119
|
|
|
1101
1120
|
return this.executeScript(el => {
|
|
@@ -1123,7 +1142,7 @@ class WebDriver extends Helper {
|
|
|
1123
1142
|
assertElementExists(res, locator, 'Clickable element')
|
|
1124
1143
|
}
|
|
1125
1144
|
|
|
1126
|
-
const elem =
|
|
1145
|
+
const elem = selectElement(res, locator, this)
|
|
1127
1146
|
highlightActiveElement.call(this, elem)
|
|
1128
1147
|
return elem.doubleClick()
|
|
1129
1148
|
}
|
|
@@ -1143,7 +1162,7 @@ class WebDriver extends Helper {
|
|
|
1143
1162
|
assertElementExists(res, locator, 'Clickable element')
|
|
1144
1163
|
}
|
|
1145
1164
|
|
|
1146
|
-
const el =
|
|
1165
|
+
const el = selectElement(res, locator, this)
|
|
1147
1166
|
|
|
1148
1167
|
await el.moveTo()
|
|
1149
1168
|
|
|
@@ -1255,10 +1274,10 @@ class WebDriver extends Helper {
|
|
|
1255
1274
|
* {{ custom }}
|
|
1256
1275
|
*
|
|
1257
1276
|
*/
|
|
1258
|
-
async fillField(field, value) {
|
|
1259
|
-
const res = await findFields.call(this, field)
|
|
1277
|
+
async fillField(field, value, context = null) {
|
|
1278
|
+
const res = await findFields.call(this, field, context)
|
|
1260
1279
|
assertElementExists(res, field, 'Field')
|
|
1261
|
-
const elem =
|
|
1280
|
+
const elem = selectElement(res, field, this)
|
|
1262
1281
|
highlightActiveElement.call(this, elem)
|
|
1263
1282
|
try {
|
|
1264
1283
|
await elem.clearValue()
|
|
@@ -1278,10 +1297,10 @@ class WebDriver extends Helper {
|
|
|
1278
1297
|
* {{> appendField }}
|
|
1279
1298
|
* {{ react }}
|
|
1280
1299
|
*/
|
|
1281
|
-
async appendField(field, value) {
|
|
1282
|
-
const res = await findFields.call(this, field)
|
|
1300
|
+
async appendField(field, value, context = null) {
|
|
1301
|
+
const res = await findFields.call(this, field, context)
|
|
1283
1302
|
assertElementExists(res, field, 'Field')
|
|
1284
|
-
const elem =
|
|
1303
|
+
const elem = selectElement(res, field, this)
|
|
1285
1304
|
highlightActiveElement.call(this, elem)
|
|
1286
1305
|
return elem.addValue(value.toString())
|
|
1287
1306
|
}
|
|
@@ -1290,10 +1309,10 @@ class WebDriver extends Helper {
|
|
|
1290
1309
|
* {{> clearField }}
|
|
1291
1310
|
*
|
|
1292
1311
|
*/
|
|
1293
|
-
async clearField(field) {
|
|
1294
|
-
const res = await findFields.call(this, field)
|
|
1312
|
+
async clearField(field, context = null) {
|
|
1313
|
+
const res = await findFields.call(this, field, context)
|
|
1295
1314
|
assertElementExists(res, field, 'Field')
|
|
1296
|
-
const elem =
|
|
1315
|
+
const elem = selectElement(res, field, this)
|
|
1297
1316
|
highlightActiveElement.call(this, elem)
|
|
1298
1317
|
return elem.clearValue(getElementId(elem))
|
|
1299
1318
|
}
|
|
@@ -1301,30 +1320,31 @@ class WebDriver extends Helper {
|
|
|
1301
1320
|
/**
|
|
1302
1321
|
* {{> selectOption }}
|
|
1303
1322
|
*/
|
|
1304
|
-
async selectOption(select, option) {
|
|
1323
|
+
async selectOption(select, option, context = null) {
|
|
1324
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1305
1325
|
const matchedLocator = new Locator(select)
|
|
1306
1326
|
|
|
1307
1327
|
// Strict locator
|
|
1308
1328
|
if (!matchedLocator.isFuzzy()) {
|
|
1309
1329
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1310
|
-
const els = await
|
|
1330
|
+
const els = await locateFn(select)
|
|
1311
1331
|
assertElementExists(els, select, 'Selectable element')
|
|
1312
|
-
return proceedSelectOption.call(this,
|
|
1332
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1313
1333
|
}
|
|
1314
1334
|
|
|
1315
1335
|
// Fuzzy: try combobox
|
|
1316
1336
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1317
1337
|
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1318
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1338
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1319
1339
|
|
|
1320
1340
|
// Fuzzy: try listbox
|
|
1321
1341
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1322
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1342
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1323
1343
|
|
|
1324
1344
|
// Fuzzy: try native select
|
|
1325
|
-
const res = await findFields.call(this, select)
|
|
1345
|
+
const res = await findFields.call(this, select, context)
|
|
1326
1346
|
assertElementExists(res, select, 'Selectable field')
|
|
1327
|
-
return proceedSelectOption.call(this,
|
|
1347
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1328
1348
|
}
|
|
1329
1349
|
|
|
1330
1350
|
/**
|
|
@@ -1332,28 +1352,41 @@ class WebDriver extends Helper {
|
|
|
1332
1352
|
*
|
|
1333
1353
|
* {{> attachFile }}
|
|
1334
1354
|
*/
|
|
1335
|
-
async attachFile(locator, pathToFile) {
|
|
1355
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1336
1356
|
let file = path.join(global.codecept_dir, pathToFile)
|
|
1337
1357
|
if (!fileExists(file)) {
|
|
1338
1358
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1339
1359
|
}
|
|
1340
1360
|
|
|
1341
|
-
const res = await findFields.call(this, locator)
|
|
1361
|
+
const res = await findFields.call(this, locator, context)
|
|
1342
1362
|
this.debug(`Uploading ${file}`)
|
|
1343
|
-
assertElementExists(res, locator, 'File field')
|
|
1344
|
-
const el = usingFirstElement(res)
|
|
1345
1363
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1364
|
+
if (res.length) {
|
|
1365
|
+
const el = selectElement(res, locator, this)
|
|
1366
|
+
const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
|
|
1367
|
+
const type = await this.browser.execute(function (elem) { return elem.type }, el)
|
|
1368
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
1369
|
+
if (this.options.remoteFileUpload) {
|
|
1370
|
+
try {
|
|
1371
|
+
this.debugSection('File', 'Uploading file to remote server')
|
|
1372
|
+
file = await this.browser.uploadFile(file)
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return el.addValue(file)
|
|
1353
1378
|
}
|
|
1354
1379
|
}
|
|
1355
1380
|
|
|
1356
|
-
|
|
1381
|
+
const targetRes = res.length ? res : await this._locate(locator)
|
|
1382
|
+
assertElementExists(targetRes, locator, 'Element')
|
|
1383
|
+
const targetEl = selectElement(targetRes, locator, this)
|
|
1384
|
+
const fileData = {
|
|
1385
|
+
base64Content: base64EncodeFile(file),
|
|
1386
|
+
fileName: path.basename(file),
|
|
1387
|
+
mimeType: getMimeType(path.basename(file)),
|
|
1388
|
+
}
|
|
1389
|
+
return this.browser.execute(dropFile, targetEl, fileData)
|
|
1357
1390
|
}
|
|
1358
1391
|
|
|
1359
1392
|
/**
|
|
@@ -1367,7 +1400,7 @@ class WebDriver extends Helper {
|
|
|
1367
1400
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1368
1401
|
|
|
1369
1402
|
assertElementExists(res, field, 'Checkable')
|
|
1370
|
-
const elem =
|
|
1403
|
+
const elem = selectElement(res, field, this)
|
|
1371
1404
|
const elementId = getElementId(elem)
|
|
1372
1405
|
highlightActiveElement.call(this, elem)
|
|
1373
1406
|
|
|
@@ -1388,7 +1421,7 @@ class WebDriver extends Helper {
|
|
|
1388
1421
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1389
1422
|
|
|
1390
1423
|
assertElementExists(res, field, 'Checkable')
|
|
1391
|
-
const elem =
|
|
1424
|
+
const elem = selectElement(res, field, this)
|
|
1392
1425
|
const elementId = getElementId(elem)
|
|
1393
1426
|
highlightActiveElement.call(this, elem)
|
|
1394
1427
|
|
|
@@ -1586,18 +1619,18 @@ class WebDriver extends Helper {
|
|
|
1586
1619
|
* {{> seeInField }}
|
|
1587
1620
|
*
|
|
1588
1621
|
*/
|
|
1589
|
-
async seeInField(field, value) {
|
|
1622
|
+
async seeInField(field, value, context = null) {
|
|
1590
1623
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1591
|
-
return proceedSeeField.call(this, 'assert', field, _value)
|
|
1624
|
+
return proceedSeeField.call(this, 'assert', field, _value, context)
|
|
1592
1625
|
}
|
|
1593
1626
|
|
|
1594
1627
|
/**
|
|
1595
1628
|
* {{> dontSeeInField }}
|
|
1596
1629
|
*
|
|
1597
1630
|
*/
|
|
1598
|
-
async dontSeeInField(field, value) {
|
|
1631
|
+
async dontSeeInField(field, value, context = null) {
|
|
1599
1632
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1600
|
-
return proceedSeeField.call(this, 'negate', field, _value)
|
|
1633
|
+
return proceedSeeField.call(this, 'negate', field, _value, context)
|
|
1601
1634
|
}
|
|
1602
1635
|
|
|
1603
1636
|
/**
|
|
@@ -1621,8 +1654,9 @@ class WebDriver extends Helper {
|
|
|
1621
1654
|
* {{ react }}
|
|
1622
1655
|
*
|
|
1623
1656
|
*/
|
|
1624
|
-
async seeElement(locator) {
|
|
1625
|
-
const
|
|
1657
|
+
async seeElement(locator, context = null) {
|
|
1658
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1659
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, true)
|
|
1626
1660
|
assertElementExists(res, locator)
|
|
1627
1661
|
const selected = await forEachAsync(res, async el => el.isDisplayed())
|
|
1628
1662
|
try {
|
|
@@ -1636,8 +1670,9 @@ class WebDriver extends Helper {
|
|
|
1636
1670
|
* {{> dontSeeElement }}
|
|
1637
1671
|
* {{ react }}
|
|
1638
1672
|
*/
|
|
1639
|
-
async dontSeeElement(locator) {
|
|
1640
|
-
const
|
|
1673
|
+
async dontSeeElement(locator, context = null) {
|
|
1674
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1675
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, false)
|
|
1641
1676
|
if (!res || res.length === 0) {
|
|
1642
1677
|
return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
|
|
1643
1678
|
}
|
|
@@ -1844,6 +1879,26 @@ class WebDriver extends Helper {
|
|
|
1844
1879
|
return urlEquals(this.options.url).negate(url, decodeUrl(res))
|
|
1845
1880
|
}
|
|
1846
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* {{> seeCurrentPathEquals }}
|
|
1884
|
+
*/
|
|
1885
|
+
async seeCurrentPathEquals(path) {
|
|
1886
|
+
const currentUrl = await this.browser.getUrl()
|
|
1887
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1888
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1889
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
1894
|
+
*/
|
|
1895
|
+
async dontSeeCurrentPathEquals(path) {
|
|
1896
|
+
const currentUrl = await this.browser.getUrl()
|
|
1897
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
1898
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1899
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1847
1902
|
/**
|
|
1848
1903
|
* Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
|
|
1849
1904
|
*
|
|
@@ -1916,8 +1971,22 @@ class WebDriver extends Helper {
|
|
|
1916
1971
|
* {{> moveCursorTo }}
|
|
1917
1972
|
*/
|
|
1918
1973
|
async moveCursorTo(locator, xOffset, yOffset) {
|
|
1919
|
-
|
|
1920
|
-
|
|
1974
|
+
let context = null
|
|
1975
|
+
if (typeof xOffset !== 'number' && xOffset !== undefined) {
|
|
1976
|
+
context = xOffset
|
|
1977
|
+
xOffset = undefined
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
let res
|
|
1981
|
+
if (context) {
|
|
1982
|
+
const contextRes = await this._locate(withStrictLocator(context), true)
|
|
1983
|
+
assertElementExists(contextRes, context, 'Context element')
|
|
1984
|
+
res = await contextRes[0].$$(withStrictLocator(locator))
|
|
1985
|
+
assertElementExists(res, locator)
|
|
1986
|
+
} else {
|
|
1987
|
+
res = await this._locate(withStrictLocator(locator), true)
|
|
1988
|
+
assertElementExists(res, locator)
|
|
1989
|
+
}
|
|
1921
1990
|
const elem = usingFirstElement(res)
|
|
1922
1991
|
try {
|
|
1923
1992
|
await elem.moveTo({ xOffset, yOffset })
|
|
@@ -2169,6 +2238,7 @@ class WebDriver extends Helper {
|
|
|
2169
2238
|
* {{> pressKeyWithKeyNormalization }}
|
|
2170
2239
|
*/
|
|
2171
2240
|
async pressKey(key) {
|
|
2241
|
+
await checkFocusBeforePressKey(this, key)
|
|
2172
2242
|
const modifiers = []
|
|
2173
2243
|
if (Array.isArray(key)) {
|
|
2174
2244
|
for (let k of key) {
|
|
@@ -2215,6 +2285,8 @@ class WebDriver extends Helper {
|
|
|
2215
2285
|
* {{> type }}
|
|
2216
2286
|
*/
|
|
2217
2287
|
async type(keys, delay = null) {
|
|
2288
|
+
await checkFocusBeforeType(this)
|
|
2289
|
+
|
|
2218
2290
|
if (!Array.isArray(keys)) {
|
|
2219
2291
|
keys = keys.toString()
|
|
2220
2292
|
keys = keys.split('')
|
|
@@ -2467,6 +2539,7 @@ class WebDriver extends Helper {
|
|
|
2467
2539
|
async waitInUrl(urlPart, sec = null) {
|
|
2468
2540
|
const client = this.browser
|
|
2469
2541
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2542
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2470
2543
|
let currUrl = ''
|
|
2471
2544
|
|
|
2472
2545
|
return client
|
|
@@ -2474,7 +2547,7 @@ class WebDriver extends Helper {
|
|
|
2474
2547
|
function () {
|
|
2475
2548
|
return this.getUrl().then(res => {
|
|
2476
2549
|
currUrl = decodeUrl(res)
|
|
2477
|
-
return currUrl.indexOf(
|
|
2550
|
+
return currUrl.indexOf(expectedUrl) > -1
|
|
2478
2551
|
})
|
|
2479
2552
|
},
|
|
2480
2553
|
{ timeout: aSec * 1000 },
|
|
@@ -2482,7 +2555,7 @@ class WebDriver extends Helper {
|
|
|
2482
2555
|
.catch(e => {
|
|
2483
2556
|
e = wrapError(e)
|
|
2484
2557
|
if (e.message.indexOf('timeout')) {
|
|
2485
|
-
throw new Error(`expected url to include ${
|
|
2558
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2486
2559
|
}
|
|
2487
2560
|
throw e
|
|
2488
2561
|
})
|
|
@@ -2493,22 +2566,47 @@ class WebDriver extends Helper {
|
|
|
2493
2566
|
*/
|
|
2494
2567
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2495
2568
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2496
|
-
const
|
|
2497
|
-
if (urlPart.indexOf('http') < 0) {
|
|
2498
|
-
urlPart = baseUrl + urlPart
|
|
2499
|
-
}
|
|
2569
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2500
2570
|
let currUrl = ''
|
|
2501
2571
|
return this.browser
|
|
2502
2572
|
.waitUntil(function () {
|
|
2503
2573
|
return this.getUrl().then(res => {
|
|
2504
2574
|
currUrl = decodeUrl(res)
|
|
2505
|
-
return currUrl ===
|
|
2575
|
+
return currUrl === expectedUrl
|
|
2506
2576
|
})
|
|
2507
2577
|
}, aSec * 1000)
|
|
2508
2578
|
.catch(e => {
|
|
2509
2579
|
e = wrapError(e)
|
|
2510
2580
|
if (e.message.indexOf('timeout')) {
|
|
2511
|
-
throw new Error(`expected url to be ${
|
|
2581
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
2582
|
+
}
|
|
2583
|
+
throw e
|
|
2584
|
+
})
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
/**
|
|
2588
|
+
* {{> waitCurrentPathEquals }}
|
|
2589
|
+
*/
|
|
2590
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
2591
|
+
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2592
|
+
const normalizedPath = normalizePath(path)
|
|
2593
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2594
|
+
let actualPath = ''
|
|
2595
|
+
|
|
2596
|
+
return this.browser
|
|
2597
|
+
.waitUntil(
|
|
2598
|
+
async () => {
|
|
2599
|
+
const currUrl = await this.browser.getUrl()
|
|
2600
|
+
const url = new URL(currUrl, baseUrl)
|
|
2601
|
+
actualPath = url.pathname
|
|
2602
|
+
return normalizePath(actualPath) === normalizedPath
|
|
2603
|
+
},
|
|
2604
|
+
{ timeout: aSec * 1000 },
|
|
2605
|
+
)
|
|
2606
|
+
.catch(e => {
|
|
2607
|
+
e = wrapError(e)
|
|
2608
|
+
if (e.message.indexOf('timeout')) {
|
|
2609
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2512
2610
|
}
|
|
2513
2611
|
throw e
|
|
2514
2612
|
})
|
|
@@ -2990,32 +3088,33 @@ async function findClickable(locator, locateFn) {
|
|
|
2990
3088
|
return await locateFn(locator.value) // by css or xpath
|
|
2991
3089
|
}
|
|
2992
3090
|
|
|
2993
|
-
async function findFields(locator) {
|
|
3091
|
+
async function findFields(locator, context = null) {
|
|
3092
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
2994
3093
|
locator = new Locator(locator)
|
|
2995
3094
|
|
|
2996
3095
|
if (this._isCustomLocator(locator)) {
|
|
2997
|
-
return
|
|
3096
|
+
return locateFn(locator)
|
|
2998
3097
|
}
|
|
2999
3098
|
|
|
3000
|
-
if (locator.isAccessibilityId() && !this.isWeb) return
|
|
3001
|
-
if (locator.isRole()) return
|
|
3002
|
-
if (!locator.isFuzzy()) return
|
|
3099
|
+
if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator)
|
|
3100
|
+
if (locator.isRole()) return locateFn(locator)
|
|
3101
|
+
if (!locator.isFuzzy()) return locateFn(locator)
|
|
3003
3102
|
|
|
3004
3103
|
const literal = xpathLocator.literal(locator.value)
|
|
3005
|
-
let els = await
|
|
3104
|
+
let els = await locateFn(Locator.field.labelEquals(literal))
|
|
3006
3105
|
if (els.length) return els
|
|
3007
3106
|
|
|
3008
|
-
els = await
|
|
3107
|
+
els = await locateFn(Locator.field.labelContains(literal))
|
|
3009
3108
|
if (els.length) return els
|
|
3010
3109
|
|
|
3011
|
-
els = await
|
|
3110
|
+
els = await locateFn(Locator.field.byName(literal))
|
|
3012
3111
|
if (els.length) return els
|
|
3013
3112
|
|
|
3014
|
-
return await
|
|
3113
|
+
return await locateFn(locator.value) // by css or xpath
|
|
3015
3114
|
}
|
|
3016
3115
|
|
|
3017
|
-
async function proceedSeeField(assertType, field, value) {
|
|
3018
|
-
const res = await findFields.call(this, field)
|
|
3116
|
+
async function proceedSeeField(assertType, field, value, context) {
|
|
3117
|
+
const res = await findFields.call(this, field, context)
|
|
3019
3118
|
assertElementExists(res, field, 'Field')
|
|
3020
3119
|
const elem = usingFirstElement(res)
|
|
3021
3120
|
const elemId = getElementId(elem)
|
|
@@ -3200,10 +3299,30 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3200
3299
|
}
|
|
3201
3300
|
|
|
3202
3301
|
function usingFirstElement(els) {
|
|
3302
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
3303
|
+
if (rawIndex != null && els.length > 1) {
|
|
3304
|
+
let elementIndex = rawIndex
|
|
3305
|
+
if (elementIndex === 'first') elementIndex = 1
|
|
3306
|
+
if (elementIndex === 'last') elementIndex = -1
|
|
3307
|
+
if (Number.isInteger(elementIndex) && elementIndex !== 0) {
|
|
3308
|
+
const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
|
|
3309
|
+
if (idx >= 0 && idx < els.length) {
|
|
3310
|
+
debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
|
|
3311
|
+
return els[idx]
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3203
3315
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3204
3316
|
return els[0]
|
|
3205
3317
|
}
|
|
3206
3318
|
|
|
3319
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
3320
|
+
if (elements.length > 1) {
|
|
3321
|
+
const webElements = Array.from(elements).map(el => new WebElement(el, helper))
|
|
3322
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3207
3326
|
function getElementId(el) {
|
|
3208
3327
|
// W3C WebDriver web element identifier
|
|
3209
3328
|
// 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
|