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/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,34 +1319,31 @@ class WebDriver extends Helper {
|
|
|
1301
1319
|
/**
|
|
1302
1320
|
* {{> selectOption }}
|
|
1303
1321
|
*/
|
|
1304
|
-
async selectOption(select, option) {
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
const elem = usingFirstElement(res)
|
|
1308
|
-
highlightActiveElement.call(this, elem)
|
|
1322
|
+
async selectOption(select, option, context = null) {
|
|
1323
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1324
|
+
const matchedLocator = new Locator(select)
|
|
1309
1325
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1326
|
+
// Strict locator
|
|
1327
|
+
if (!matchedLocator.isFuzzy()) {
|
|
1328
|
+
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1329
|
+
const els = await locateFn(select)
|
|
1330
|
+
assertElementExists(els, select, 'Selectable element')
|
|
1331
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1312
1332
|
}
|
|
1313
1333
|
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1334
|
+
// Fuzzy: try combobox
|
|
1335
|
+
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1336
|
+
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1337
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1316
1338
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (elementId) return this.browser.elementClick(elementId)
|
|
1321
|
-
}
|
|
1339
|
+
// Fuzzy: try listbox
|
|
1340
|
+
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1341
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1322
1342
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
|
|
1328
|
-
if (els.length === 0) {
|
|
1329
|
-
throw new ElementNotFound(select, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
|
|
1330
|
-
}
|
|
1331
|
-
return forEachAsync(els, clickOptionFn)
|
|
1343
|
+
// Fuzzy: try native select
|
|
1344
|
+
const res = await findFields.call(this, select, context)
|
|
1345
|
+
assertElementExists(res, select, 'Selectable field')
|
|
1346
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1332
1347
|
}
|
|
1333
1348
|
|
|
1334
1349
|
/**
|
|
@@ -1336,28 +1351,41 @@ class WebDriver extends Helper {
|
|
|
1336
1351
|
*
|
|
1337
1352
|
* {{> attachFile }}
|
|
1338
1353
|
*/
|
|
1339
|
-
async attachFile(locator, pathToFile) {
|
|
1354
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1340
1355
|
let file = path.join(global.codecept_dir, pathToFile)
|
|
1341
1356
|
if (!fileExists(file)) {
|
|
1342
1357
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1343
1358
|
}
|
|
1344
1359
|
|
|
1345
|
-
const res = await findFields.call(this, locator)
|
|
1360
|
+
const res = await findFields.call(this, locator, context)
|
|
1346
1361
|
this.debug(`Uploading ${file}`)
|
|
1347
|
-
assertElementExists(res, locator, 'File field')
|
|
1348
|
-
const el = usingFirstElement(res)
|
|
1349
1362
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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)
|
|
1357
1377
|
}
|
|
1358
1378
|
}
|
|
1359
1379
|
|
|
1360
|
-
|
|
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)
|
|
1361
1389
|
}
|
|
1362
1390
|
|
|
1363
1391
|
/**
|
|
@@ -1371,7 +1399,7 @@ class WebDriver extends Helper {
|
|
|
1371
1399
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1372
1400
|
|
|
1373
1401
|
assertElementExists(res, field, 'Checkable')
|
|
1374
|
-
const elem =
|
|
1402
|
+
const elem = selectElement(res, field, this)
|
|
1375
1403
|
const elementId = getElementId(elem)
|
|
1376
1404
|
highlightActiveElement.call(this, elem)
|
|
1377
1405
|
|
|
@@ -1392,7 +1420,7 @@ class WebDriver extends Helper {
|
|
|
1392
1420
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1393
1421
|
|
|
1394
1422
|
assertElementExists(res, field, 'Checkable')
|
|
1395
|
-
const elem =
|
|
1423
|
+
const elem = selectElement(res, field, this)
|
|
1396
1424
|
const elementId = getElementId(elem)
|
|
1397
1425
|
highlightActiveElement.call(this, elem)
|
|
1398
1426
|
|
|
@@ -1590,18 +1618,18 @@ class WebDriver extends Helper {
|
|
|
1590
1618
|
* {{> seeInField }}
|
|
1591
1619
|
*
|
|
1592
1620
|
*/
|
|
1593
|
-
async seeInField(field, value) {
|
|
1621
|
+
async seeInField(field, value, context = null) {
|
|
1594
1622
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1595
|
-
return proceedSeeField.call(this, 'assert', field, _value)
|
|
1623
|
+
return proceedSeeField.call(this, 'assert', field, _value, context)
|
|
1596
1624
|
}
|
|
1597
1625
|
|
|
1598
1626
|
/**
|
|
1599
1627
|
* {{> dontSeeInField }}
|
|
1600
1628
|
*
|
|
1601
1629
|
*/
|
|
1602
|
-
async dontSeeInField(field, value) {
|
|
1630
|
+
async dontSeeInField(field, value, context = null) {
|
|
1603
1631
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1604
|
-
return proceedSeeField.call(this, 'negate', field, _value)
|
|
1632
|
+
return proceedSeeField.call(this, 'negate', field, _value, context)
|
|
1605
1633
|
}
|
|
1606
1634
|
|
|
1607
1635
|
/**
|
|
@@ -1625,8 +1653,9 @@ class WebDriver extends Helper {
|
|
|
1625
1653
|
* {{ react }}
|
|
1626
1654
|
*
|
|
1627
1655
|
*/
|
|
1628
|
-
async seeElement(locator) {
|
|
1629
|
-
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)
|
|
1630
1659
|
assertElementExists(res, locator)
|
|
1631
1660
|
const selected = await forEachAsync(res, async el => el.isDisplayed())
|
|
1632
1661
|
try {
|
|
@@ -1640,8 +1669,9 @@ class WebDriver extends Helper {
|
|
|
1640
1669
|
* {{> dontSeeElement }}
|
|
1641
1670
|
* {{ react }}
|
|
1642
1671
|
*/
|
|
1643
|
-
async dontSeeElement(locator) {
|
|
1644
|
-
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)
|
|
1645
1675
|
if (!res || res.length === 0) {
|
|
1646
1676
|
return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
|
|
1647
1677
|
}
|
|
@@ -1848,6 +1878,26 @@ class WebDriver extends Helper {
|
|
|
1848
1878
|
return urlEquals(this.options.url).negate(url, decodeUrl(res))
|
|
1849
1879
|
}
|
|
1850
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
|
+
|
|
1851
1901
|
/**
|
|
1852
1902
|
* Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
|
|
1853
1903
|
*
|
|
@@ -1920,8 +1970,22 @@ class WebDriver extends Helper {
|
|
|
1920
1970
|
* {{> moveCursorTo }}
|
|
1921
1971
|
*/
|
|
1922
1972
|
async moveCursorTo(locator, xOffset, yOffset) {
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
+
}
|
|
1925
1989
|
const elem = usingFirstElement(res)
|
|
1926
1990
|
try {
|
|
1927
1991
|
await elem.moveTo({ xOffset, yOffset })
|
|
@@ -2471,6 +2535,7 @@ class WebDriver extends Helper {
|
|
|
2471
2535
|
async waitInUrl(urlPart, sec = null) {
|
|
2472
2536
|
const client = this.browser
|
|
2473
2537
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2538
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2474
2539
|
let currUrl = ''
|
|
2475
2540
|
|
|
2476
2541
|
return client
|
|
@@ -2478,7 +2543,7 @@ class WebDriver extends Helper {
|
|
|
2478
2543
|
function () {
|
|
2479
2544
|
return this.getUrl().then(res => {
|
|
2480
2545
|
currUrl = decodeUrl(res)
|
|
2481
|
-
return currUrl.indexOf(
|
|
2546
|
+
return currUrl.indexOf(expectedUrl) > -1
|
|
2482
2547
|
})
|
|
2483
2548
|
},
|
|
2484
2549
|
{ timeout: aSec * 1000 },
|
|
@@ -2486,7 +2551,7 @@ class WebDriver extends Helper {
|
|
|
2486
2551
|
.catch(e => {
|
|
2487
2552
|
e = wrapError(e)
|
|
2488
2553
|
if (e.message.indexOf('timeout')) {
|
|
2489
|
-
throw new Error(`expected url to include ${
|
|
2554
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2490
2555
|
}
|
|
2491
2556
|
throw e
|
|
2492
2557
|
})
|
|
@@ -2497,22 +2562,47 @@ class WebDriver extends Helper {
|
|
|
2497
2562
|
*/
|
|
2498
2563
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2499
2564
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2500
|
-
const
|
|
2501
|
-
if (urlPart.indexOf('http') < 0) {
|
|
2502
|
-
urlPart = baseUrl + urlPart
|
|
2503
|
-
}
|
|
2565
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2504
2566
|
let currUrl = ''
|
|
2505
2567
|
return this.browser
|
|
2506
2568
|
.waitUntil(function () {
|
|
2507
2569
|
return this.getUrl().then(res => {
|
|
2508
2570
|
currUrl = decodeUrl(res)
|
|
2509
|
-
return currUrl ===
|
|
2571
|
+
return currUrl === expectedUrl
|
|
2510
2572
|
})
|
|
2511
2573
|
}, aSec * 1000)
|
|
2512
2574
|
.catch(e => {
|
|
2513
2575
|
e = wrapError(e)
|
|
2514
2576
|
if (e.message.indexOf('timeout')) {
|
|
2515
|
-
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)}`)
|
|
2516
2606
|
}
|
|
2517
2607
|
throw e
|
|
2518
2608
|
})
|
|
@@ -2994,32 +3084,33 @@ async function findClickable(locator, locateFn) {
|
|
|
2994
3084
|
return await locateFn(locator.value) // by css or xpath
|
|
2995
3085
|
}
|
|
2996
3086
|
|
|
2997
|
-
async function findFields(locator) {
|
|
3087
|
+
async function findFields(locator, context = null) {
|
|
3088
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
2998
3089
|
locator = new Locator(locator)
|
|
2999
3090
|
|
|
3000
3091
|
if (this._isCustomLocator(locator)) {
|
|
3001
|
-
return
|
|
3092
|
+
return locateFn(locator)
|
|
3002
3093
|
}
|
|
3003
3094
|
|
|
3004
|
-
if (locator.isAccessibilityId() && !this.isWeb) return
|
|
3005
|
-
if (locator.isRole()) return
|
|
3006
|
-
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)
|
|
3007
3098
|
|
|
3008
3099
|
const literal = xpathLocator.literal(locator.value)
|
|
3009
|
-
let els = await
|
|
3100
|
+
let els = await locateFn(Locator.field.labelEquals(literal))
|
|
3010
3101
|
if (els.length) return els
|
|
3011
3102
|
|
|
3012
|
-
els = await
|
|
3103
|
+
els = await locateFn(Locator.field.labelContains(literal))
|
|
3013
3104
|
if (els.length) return els
|
|
3014
3105
|
|
|
3015
|
-
els = await
|
|
3106
|
+
els = await locateFn(Locator.field.byName(literal))
|
|
3016
3107
|
if (els.length) return els
|
|
3017
3108
|
|
|
3018
|
-
return await
|
|
3109
|
+
return await locateFn(locator.value) // by css or xpath
|
|
3019
3110
|
}
|
|
3020
3111
|
|
|
3021
|
-
async function proceedSeeField(assertType, field, value) {
|
|
3022
|
-
const res = await findFields.call(this, field)
|
|
3112
|
+
async function proceedSeeField(assertType, field, value, context) {
|
|
3113
|
+
const res = await findFields.call(this, field, context)
|
|
3023
3114
|
assertElementExists(res, field, 'Field')
|
|
3024
3115
|
const elem = usingFirstElement(res)
|
|
3025
3116
|
const elemId = getElementId(elem)
|
|
@@ -3128,7 +3219,23 @@ async function getElementTextAttributes(element) {
|
|
|
3128
3219
|
const ariaLabel = await this.browser.getElementAttribute(elementId, 'aria-label').catch(() => '')
|
|
3129
3220
|
const placeholder = await this.browser.getElementAttribute(elementId, 'placeholder').catch(() => '')
|
|
3130
3221
|
const innerText = await this.browser.getElementText(elementId).catch(() => '')
|
|
3131
|
-
|
|
3222
|
+
|
|
3223
|
+
// Handle aria-labelledby
|
|
3224
|
+
const labelledBy = await this.browser.getElementAttribute(elementId, 'aria-labelledby').catch(() => '')
|
|
3225
|
+
let labelText = ''
|
|
3226
|
+
if (labelledBy) {
|
|
3227
|
+
try {
|
|
3228
|
+
const labelId = labelledBy.split(' ')[0]
|
|
3229
|
+
const labelEls = await this.browser.$$(`#${labelId}`)
|
|
3230
|
+
if (labelEls?.length) {
|
|
3231
|
+
labelText = await this.browser.getElementText(getElementId(labelEls[0])).catch(() => '')
|
|
3232
|
+
}
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
// Ignore errors when resolving aria-labelledby
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return [ariaLabel, placeholder, innerText, labelText]
|
|
3132
3239
|
}
|
|
3133
3240
|
|
|
3134
3241
|
async function isElementChecked(browser, elementId) {
|
|
@@ -3188,10 +3295,30 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3188
3295
|
}
|
|
3189
3296
|
|
|
3190
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
|
+
}
|
|
3191
3311
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3192
3312
|
return els[0]
|
|
3193
3313
|
}
|
|
3194
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
|
+
|
|
3195
3322
|
function getElementId(el) {
|
|
3196
3323
|
// W3C WebDriver web element identifier
|
|
3197
3324
|
// https://w3c.github.io/webdriver/#dfn-web-element-identifier
|
|
@@ -3376,4 +3503,107 @@ function logEvents(event) {
|
|
|
3376
3503
|
browserLogs.push(event.text) // add log message to the array
|
|
3377
3504
|
}
|
|
3378
3505
|
|
|
3506
|
+
async function proceedSelectOption(elem, option) {
|
|
3507
|
+
const elementId = getElementId(elem)
|
|
3508
|
+
const role = await this.browser.getElementAttribute(elementId, 'role').catch(() => null)
|
|
3509
|
+
const options = Array.isArray(option) ? option : [option]
|
|
3510
|
+
|
|
3511
|
+
if (role === 'combobox') {
|
|
3512
|
+
this.debugSection('SelectOption', 'Expanding combobox')
|
|
3513
|
+
highlightActiveElement.call(this, elem)
|
|
3514
|
+
const ariaOwns = await this.browser.getElementAttribute(elementId, 'aria-owns').catch(() => null)
|
|
3515
|
+
const ariaControls = await this.browser.getElementAttribute(elementId, 'aria-controls').catch(() => null)
|
|
3516
|
+
const ariaLabelledBy = await this.browser.getElementAttribute(elementId, 'aria-labelledby').catch(() => null)
|
|
3517
|
+
await this.browser.elementClick(elementId)
|
|
3518
|
+
|
|
3519
|
+
const listboxId = ariaOwns || ariaControls
|
|
3520
|
+
let listbox = null
|
|
3521
|
+
if (listboxId) {
|
|
3522
|
+
const listboxEls = await this.browser.$$(`#${listboxId}`)
|
|
3523
|
+
if (listboxEls?.length) listbox = listboxEls[0]
|
|
3524
|
+
}
|
|
3525
|
+
if (!listbox && ariaLabelledBy) {
|
|
3526
|
+
// Find listbox with the same aria-labelledby
|
|
3527
|
+
const listboxEls = await this.browser.$$(`[role="listbox"][aria-labelledby="${ariaLabelledBy}"]`)
|
|
3528
|
+
if (listboxEls?.length) listbox = listboxEls[0]
|
|
3529
|
+
}
|
|
3530
|
+
if (!listbox) {
|
|
3531
|
+
// Fallback: find any listbox with the same label
|
|
3532
|
+
const allListboxes = await this.browser.$$(`[role="listbox"]`)
|
|
3533
|
+
for (const lb of allListboxes) {
|
|
3534
|
+
const lbLabelledBy = await this.browser.getElementAttribute(getElementId(lb), 'aria-labelledby').catch(() => '')
|
|
3535
|
+
if (lbLabelledBy === ariaLabelledBy) {
|
|
3536
|
+
listbox = lb
|
|
3537
|
+
break
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
if (listbox) {
|
|
3543
|
+
const listboxElementId = getElementId(listbox)
|
|
3544
|
+
for (const opt of options) {
|
|
3545
|
+
const optEls = await this.browser.findElementsFromElement(listboxElementId, 'xpath', `.//*[@role="option"]`)
|
|
3546
|
+
if (optEls?.length) {
|
|
3547
|
+
for (const optEl of optEls) {
|
|
3548
|
+
const optElId = getElementId(optEl)
|
|
3549
|
+
const text = await this.browser.getElementText(optElId).catch(() => '')
|
|
3550
|
+
if (text && text.includes(opt)) {
|
|
3551
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3552
|
+
highlightActiveElement.call(this, optEl)
|
|
3553
|
+
await this.browser.elementClick(optElId)
|
|
3554
|
+
break
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
if (role === 'listbox') {
|
|
3564
|
+
for (const opt of options) {
|
|
3565
|
+
const optEls = await this.browser.findElementsFromElement(elementId, 'xpath', `.//*[@role="option"]`)
|
|
3566
|
+
if (optEls?.length) {
|
|
3567
|
+
for (const optEl of optEls) {
|
|
3568
|
+
const optElId = getElementId(optEl)
|
|
3569
|
+
const text = await this.browser.getElementText(optElId).catch(() => '')
|
|
3570
|
+
if (text && text.includes(opt)) {
|
|
3571
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3572
|
+
highlightActiveElement.call(this, optEl)
|
|
3573
|
+
await this.browser.elementClick(optElId)
|
|
3574
|
+
break
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// Native <select> element
|
|
3583
|
+
highlightActiveElement.call(this, elem)
|
|
3584
|
+
|
|
3585
|
+
if (!Array.isArray(option)) {
|
|
3586
|
+
option = [option]
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
const clickOptionFn = async el => {
|
|
3590
|
+
if (el[0]) el = el[0]
|
|
3591
|
+
const elId = getElementId(el)
|
|
3592
|
+
if (elId) return this.browser.elementClick(elId)
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
// select options by visible text
|
|
3596
|
+
let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
|
|
3597
|
+
|
|
3598
|
+
if (Array.isArray(els) && els.length) {
|
|
3599
|
+
return forEachAsync(els, clickOptionFn)
|
|
3600
|
+
}
|
|
3601
|
+
// select options by value
|
|
3602
|
+
els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
|
|
3603
|
+
if (els.length === 0) {
|
|
3604
|
+
throw new ElementNotFound(elem, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
|
|
3605
|
+
}
|
|
3606
|
+
return forEachAsync(els, clickOptionFn)
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3379
3609
|
export { WebDriver as default }
|
|
@@ -5,10 +5,13 @@ import Locator from '../../locator.js'
|
|
|
5
5
|
*/
|
|
6
6
|
class ElementNotFound {
|
|
7
7
|
constructor(locator, prefixMessage = 'Element', postfixMessage = 'was not found by text|CSS|XPath') {
|
|
8
|
+
let locatorStr
|
|
8
9
|
if (typeof locator === 'object') {
|
|
9
|
-
|
|
10
|
+
locatorStr = JSON.stringify(locator)
|
|
11
|
+
} else {
|
|
12
|
+
locatorStr = new Locator(locator).toString()
|
|
10
13
|
}
|
|
11
|
-
throw new Error(`${prefixMessage} "${
|
|
14
|
+
throw new Error(`${prefixMessage} "${locatorStr}" ${postfixMessage}`)
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|