codeceptjs 4.0.0-rc.1 → 4.0.0-rc.11

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