codeceptjs 4.0.0-rc.1 → 4.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +610 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/moveCursorTo.mustache +5 -1
  12. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  13. package/docs/webapi/seeElement.mustache +4 -0
  14. package/docs/webapi/seeInField.mustache +5 -0
  15. package/docs/webapi/selectOption.mustache +5 -0
  16. package/docs/webapi/uncheckOption.mustache +1 -1
  17. package/lib/codecept.js +20 -17
  18. package/lib/command/init.js +0 -3
  19. package/lib/command/run-workers.js +1 -0
  20. package/lib/container.js +19 -4
  21. package/lib/element/WebElement.js +81 -2
  22. package/lib/els.js +12 -6
  23. package/lib/helper/Appium.js +8 -8
  24. package/lib/helper/Playwright.js +219 -137
  25. package/lib/helper/Puppeteer.js +207 -69
  26. package/lib/helper/WebDriver.js +179 -64
  27. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  28. package/lib/helper/extras/elementSelection.js +58 -0
  29. package/lib/helper/scripts/dropFile.js +11 -0
  30. package/lib/html.js +14 -1
  31. package/lib/listener/globalRetry.js +32 -6
  32. package/lib/mocha/cli.js +10 -0
  33. package/lib/plugin/aiTrace.js +464 -0
  34. package/lib/plugin/retryFailedStep.js +28 -19
  35. package/lib/plugin/stepByStepReport.js +5 -1
  36. package/lib/step/config.js +15 -2
  37. package/lib/step/record.js +1 -1
  38. package/lib/utils.js +48 -0
  39. package/lib/workers.js +49 -7
  40. package/package.json +5 -3
  41. package/typings/index.d.ts +19 -0
  42. package/lib/listener/enhancedGlobalRetry.js +0 -110
  43. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  44. package/lib/plugin/htmlReporter.js +0 -3648
  45. package/lib/retryCoordinator.js +0 -207
  46. package/typings/promiseBasedTypes.d.ts +0 -9469
  47. package/typings/types.d.ts +0 -11402
@@ -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,30 +1319,31 @@ class WebDriver extends Helper {
1301
1319
  /**
1302
1320
  * {{> selectOption }}
1303
1321
  */
1304
- async selectOption(select, option) {
1322
+ async selectOption(select, option, context = null) {
1323
+ const locateFn = prepareLocateFn.call(this, context)
1305
1324
  const matchedLocator = new Locator(select)
1306
1325
 
1307
1326
  // Strict locator
1308
1327
  if (!matchedLocator.isFuzzy()) {
1309
1328
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1310
- const els = await this._locate(select)
1329
+ const els = await locateFn(select)
1311
1330
  assertElementExists(els, select, 'Selectable element')
1312
- return proceedSelectOption.call(this, usingFirstElement(els), option)
1331
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1313
1332
  }
1314
1333
 
1315
1334
  // Fuzzy: try combobox
1316
1335
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1317
1336
  let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1318
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1337
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1319
1338
 
1320
1339
  // Fuzzy: try listbox
1321
1340
  els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1322
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1341
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1323
1342
 
1324
1343
  // Fuzzy: try native select
1325
- const res = await findFields.call(this, select)
1344
+ const res = await findFields.call(this, select, context)
1326
1345
  assertElementExists(res, select, 'Selectable field')
1327
- return proceedSelectOption.call(this, usingFirstElement(res), option)
1346
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1328
1347
  }
1329
1348
 
1330
1349
  /**
@@ -1332,28 +1351,41 @@ class WebDriver extends Helper {
1332
1351
  *
1333
1352
  * {{> attachFile }}
1334
1353
  */
1335
- async attachFile(locator, pathToFile) {
1354
+ async attachFile(locator, pathToFile, context = null) {
1336
1355
  let file = path.join(global.codecept_dir, pathToFile)
1337
1356
  if (!fileExists(file)) {
1338
1357
  throw new Error(`File at ${file} can not be found on local system`)
1339
1358
  }
1340
1359
 
1341
- const res = await findFields.call(this, locator)
1360
+ const res = await findFields.call(this, locator, context)
1342
1361
  this.debug(`Uploading ${file}`)
1343
- assertElementExists(res, locator, 'File field')
1344
- const el = usingFirstElement(res)
1345
1362
 
1346
- // Remote Upload (when running Selenium Server)
1347
- if (this.options.remoteFileUpload) {
1348
- try {
1349
- this.debugSection('File', 'Uploading file to remote server')
1350
- file = await this.browser.uploadFile(file)
1351
- } catch (err) {
1352
- 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)
1353
1377
  }
1354
1378
  }
1355
1379
 
1356
- 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)
1357
1389
  }
1358
1390
 
1359
1391
  /**
@@ -1367,7 +1399,7 @@ class WebDriver extends Helper {
1367
1399
  const res = await findCheckable.call(this, field, locateFn)
1368
1400
 
1369
1401
  assertElementExists(res, field, 'Checkable')
1370
- const elem = usingFirstElement(res)
1402
+ const elem = selectElement(res, field, this)
1371
1403
  const elementId = getElementId(elem)
1372
1404
  highlightActiveElement.call(this, elem)
1373
1405
 
@@ -1388,7 +1420,7 @@ class WebDriver extends Helper {
1388
1420
  const res = await findCheckable.call(this, field, locateFn)
1389
1421
 
1390
1422
  assertElementExists(res, field, 'Checkable')
1391
- const elem = usingFirstElement(res)
1423
+ const elem = selectElement(res, field, this)
1392
1424
  const elementId = getElementId(elem)
1393
1425
  highlightActiveElement.call(this, elem)
1394
1426
 
@@ -1586,18 +1618,18 @@ class WebDriver extends Helper {
1586
1618
  * {{> seeInField }}
1587
1619
  *
1588
1620
  */
1589
- async seeInField(field, value) {
1621
+ async seeInField(field, value, context = null) {
1590
1622
  const _value = typeof value === 'boolean' ? value : value.toString()
1591
- return proceedSeeField.call(this, 'assert', field, _value)
1623
+ return proceedSeeField.call(this, 'assert', field, _value, context)
1592
1624
  }
1593
1625
 
1594
1626
  /**
1595
1627
  * {{> dontSeeInField }}
1596
1628
  *
1597
1629
  */
1598
- async dontSeeInField(field, value) {
1630
+ async dontSeeInField(field, value, context = null) {
1599
1631
  const _value = typeof value === 'boolean' ? value : value.toString()
1600
- return proceedSeeField.call(this, 'negate', field, _value)
1632
+ return proceedSeeField.call(this, 'negate', field, _value, context)
1601
1633
  }
1602
1634
 
1603
1635
  /**
@@ -1621,8 +1653,9 @@ class WebDriver extends Helper {
1621
1653
  * {{ react }}
1622
1654
  *
1623
1655
  */
1624
- async seeElement(locator) {
1625
- const 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)
1626
1659
  assertElementExists(res, locator)
1627
1660
  const selected = await forEachAsync(res, async el => el.isDisplayed())
1628
1661
  try {
@@ -1636,8 +1669,9 @@ class WebDriver extends Helper {
1636
1669
  * {{> dontSeeElement }}
1637
1670
  * {{ react }}
1638
1671
  */
1639
- async dontSeeElement(locator) {
1640
- const 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)
1641
1675
  if (!res || res.length === 0) {
1642
1676
  return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
1643
1677
  }
@@ -1844,6 +1878,26 @@ class WebDriver extends Helper {
1844
1878
  return urlEquals(this.options.url).negate(url, decodeUrl(res))
1845
1879
  }
1846
1880
 
1881
+ /**
1882
+ * {{> seeCurrentPathEquals }}
1883
+ */
1884
+ async seeCurrentPathEquals(path) {
1885
+ const currentUrl = await this.browser.getUrl()
1886
+ const baseUrl = this.options.url || 'http://localhost'
1887
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1888
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1889
+ }
1890
+
1891
+ /**
1892
+ * {{> dontSeeCurrentPathEquals }}
1893
+ */
1894
+ async dontSeeCurrentPathEquals(path) {
1895
+ const currentUrl = await this.browser.getUrl()
1896
+ const baseUrl = this.options.url || 'http://localhost'
1897
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1898
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1899
+ }
1900
+
1847
1901
  /**
1848
1902
  * Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
1849
1903
  *
@@ -1916,8 +1970,22 @@ class WebDriver extends Helper {
1916
1970
  * {{> moveCursorTo }}
1917
1971
  */
1918
1972
  async moveCursorTo(locator, xOffset, yOffset) {
1919
- const res = await this._locate(withStrictLocator(locator), true)
1920
- 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
+ }
1921
1989
  const elem = usingFirstElement(res)
1922
1990
  try {
1923
1991
  await elem.moveTo({ xOffset, yOffset })
@@ -2467,6 +2535,7 @@ class WebDriver extends Helper {
2467
2535
  async waitInUrl(urlPart, sec = null) {
2468
2536
  const client = this.browser
2469
2537
  const aSec = sec || this.options.waitForTimeoutInSeconds
2538
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2470
2539
  let currUrl = ''
2471
2540
 
2472
2541
  return client
@@ -2474,7 +2543,7 @@ class WebDriver extends Helper {
2474
2543
  function () {
2475
2544
  return this.getUrl().then(res => {
2476
2545
  currUrl = decodeUrl(res)
2477
- return currUrl.indexOf(urlPart) > -1
2546
+ return currUrl.indexOf(expectedUrl) > -1
2478
2547
  })
2479
2548
  },
2480
2549
  { timeout: aSec * 1000 },
@@ -2482,7 +2551,7 @@ class WebDriver extends Helper {
2482
2551
  .catch(e => {
2483
2552
  e = wrapError(e)
2484
2553
  if (e.message.indexOf('timeout')) {
2485
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2554
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2486
2555
  }
2487
2556
  throw e
2488
2557
  })
@@ -2493,22 +2562,47 @@ class WebDriver extends Helper {
2493
2562
  */
2494
2563
  async waitUrlEquals(urlPart, sec = null) {
2495
2564
  const aSec = sec || this.options.waitForTimeoutInSeconds
2496
- const baseUrl = this.options.url
2497
- if (urlPart.indexOf('http') < 0) {
2498
- urlPart = baseUrl + urlPart
2499
- }
2565
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2500
2566
  let currUrl = ''
2501
2567
  return this.browser
2502
2568
  .waitUntil(function () {
2503
2569
  return this.getUrl().then(res => {
2504
2570
  currUrl = decodeUrl(res)
2505
- return currUrl === urlPart
2571
+ return currUrl === expectedUrl
2506
2572
  })
2507
2573
  }, aSec * 1000)
2508
2574
  .catch(e => {
2509
2575
  e = wrapError(e)
2510
2576
  if (e.message.indexOf('timeout')) {
2511
- throw new Error(`expected url to be ${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)}`)
2512
2606
  }
2513
2607
  throw e
2514
2608
  })
@@ -2990,32 +3084,33 @@ async function findClickable(locator, locateFn) {
2990
3084
  return await locateFn(locator.value) // by css or xpath
2991
3085
  }
2992
3086
 
2993
- async function findFields(locator) {
3087
+ async function findFields(locator, context = null) {
3088
+ const locateFn = prepareLocateFn.call(this, context)
2994
3089
  locator = new Locator(locator)
2995
3090
 
2996
3091
  if (this._isCustomLocator(locator)) {
2997
- return this._locate(locator)
3092
+ return locateFn(locator)
2998
3093
  }
2999
3094
 
3000
- if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true)
3001
- if (locator.isRole()) return this._locate(locator, true)
3002
- 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)
3003
3098
 
3004
3099
  const literal = xpathLocator.literal(locator.value)
3005
- let els = await this._locate(Locator.field.labelEquals(literal))
3100
+ let els = await locateFn(Locator.field.labelEquals(literal))
3006
3101
  if (els.length) return els
3007
3102
 
3008
- els = await this._locate(Locator.field.labelContains(literal))
3103
+ els = await locateFn(Locator.field.labelContains(literal))
3009
3104
  if (els.length) return els
3010
3105
 
3011
- els = await this._locate(Locator.field.byName(literal))
3106
+ els = await locateFn(Locator.field.byName(literal))
3012
3107
  if (els.length) return els
3013
3108
 
3014
- return await this._locate(locator.value) // by css or xpath
3109
+ return await locateFn(locator.value) // by css or xpath
3015
3110
  }
3016
3111
 
3017
- async function proceedSeeField(assertType, field, value) {
3018
- const res = await findFields.call(this, field)
3112
+ async function proceedSeeField(assertType, field, value, context) {
3113
+ const res = await findFields.call(this, field, context)
3019
3114
  assertElementExists(res, field, 'Field')
3020
3115
  const elem = usingFirstElement(res)
3021
3116
  const elemId = getElementId(elem)
@@ -3200,10 +3295,30 @@ function assertElementExists(res, locator, prefix, suffix) {
3200
3295
  }
3201
3296
 
3202
3297
  function usingFirstElement(els) {
3298
+ const rawIndex = store.currentStep?.opts?.elementIndex
3299
+ if (rawIndex != null && els.length > 1) {
3300
+ let elementIndex = rawIndex
3301
+ if (elementIndex === 'first') elementIndex = 1
3302
+ if (elementIndex === 'last') elementIndex = -1
3303
+ if (Number.isInteger(elementIndex) && elementIndex !== 0) {
3304
+ const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
3305
+ if (idx >= 0 && idx < els.length) {
3306
+ debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
3307
+ return els[idx]
3308
+ }
3309
+ }
3310
+ }
3203
3311
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3204
3312
  return els[0]
3205
3313
  }
3206
3314
 
3315
+ function assertOnlyOneElement(elements, locator, helper) {
3316
+ if (elements.length > 1) {
3317
+ const webElements = Array.from(elements).map(el => new WebElement(el, helper))
3318
+ throw new MultipleElementsFound(locator, webElements)
3319
+ }
3320
+ }
3321
+
3207
3322
  function getElementId(el) {
3208
3323
  // W3C WebDriver web element identifier
3209
3324
  // https://w3c.github.io/webdriver/#dfn-web-element-identifier
@@ -1,40 +1,45 @@
1
1
  import Locator from '../../locator.js'
2
2
 
3
- /**
4
- * Error thrown when strict mode is enabled and multiple elements are found
5
- * for a single-element locator operation (click, fillField, etc.)
6
- */
7
3
  class MultipleElementsFound extends Error {
8
- /**
9
- * @param {Locator|string|object} locator - The locator used
10
- * @param {Array<HTMLElement>} elements - Array of Playwright element handles found
11
- */
12
- constructor(locator, elements) {
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.elements = elements
17
- this.count = elements.length
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
- if (typeof this.locator === 'object' && !(this.locator instanceof Locator)) {
30
- this.locator = JSON.stringify(this.locator)
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
- const locatorObj = new Locator(this.locator)
34
- const elementList = await this._generateElementList(this.elements, this.count)
34
+ if (this.count > 10) {
35
+ items.push(` ... and ${this.count - 10} more`)
36
+ }
35
37
 
36
- this.message = `Multiple elements (${this.count}) found for "${locatorObj.toString()}" in strict mode.\n` +
37
- elementList +
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