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.
Files changed (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. package/typings/types.d.ts +0 -13073
@@ -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 { xpathLocator, fileExists, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, modifierKeys } from '../utils.js'
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 res = await findFields.call(this, select)
1306
- assertElementExists(res, select, 'Selectable field')
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
- if (!Array.isArray(option)) {
1311
- option = [option]
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
- // select options by visible text
1315
- let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
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
- const clickOptionFn = async el => {
1318
- if (el[0]) el = el[0]
1319
- const elementId = getElementId(el)
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
- if (Array.isArray(els) && els.length) {
1324
- return forEachAsync(els, clickOptionFn)
1325
- }
1326
- // select options by value
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
- // Remote Upload (when running Selenium Server)
1351
- if (this.options.remoteFileUpload) {
1352
- try {
1353
- this.debugSection('File', 'Uploading file to remote server')
1354
- file = await this.browser.uploadFile(file)
1355
- } catch (err) {
1356
- throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
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
- return el.addValue(file)
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 = usingFirstElement(res)
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 = usingFirstElement(res)
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 res = await this._locate(locator, true)
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 res = await this._locate(locator, false)
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
- const res = await this._locate(withStrictLocator(locator), true)
1924
- assertElementExists(res, locator)
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(urlPart) > -1
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 ${urlPart}, but found ${currUrl}`)
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 baseUrl = this.options.url
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 === urlPart
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 ${urlPart}, but found ${currUrl}`)
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 this._locate(locator)
3092
+ return locateFn(locator)
3002
3093
  }
3003
3094
 
3004
- if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true)
3005
- if (locator.isRole()) return this._locate(locator, true)
3006
- if (!locator.isFuzzy()) return this._locate(locator, true)
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 this._locate(Locator.field.labelEquals(literal))
3100
+ let els = await locateFn(Locator.field.labelEquals(literal))
3010
3101
  if (els.length) return els
3011
3102
 
3012
- els = await this._locate(Locator.field.labelContains(literal))
3103
+ els = await locateFn(Locator.field.labelContains(literal))
3013
3104
  if (els.length) return els
3014
3105
 
3015
- els = await this._locate(Locator.field.byName(literal))
3106
+ els = await locateFn(Locator.field.byName(literal))
3016
3107
  if (els.length) return els
3017
3108
 
3018
- return await this._locate(locator.value) // by css or xpath
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
- return [ariaLabel, placeholder, innerText]
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
- locator = JSON.stringify(locator)
10
+ locatorStr = JSON.stringify(locator)
11
+ } else {
12
+ locatorStr = new Locator(locator).toString()
10
13
  }
11
- throw new Error(`${prefixMessage} "${new Locator(locator)}" ${postfixMessage}`)
14
+ throw new Error(`${prefixMessage} "${locatorStr}" ${postfixMessage}`)
12
15
  }
13
16
  }
14
17