codeceptjs 4.0.0-rc.8 → 4.0.0-rc.9

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.
@@ -325,7 +325,7 @@ class WebElement {
325
325
  parts.unshift(`${tagName}${pathIndex}`)
326
326
  current = current.parentElement
327
327
  }
328
- return '/' + parts.join('/')
328
+ return '//' + parts.join('/')
329
329
  }
330
330
 
331
331
  switch (this.helperType) {
@@ -38,6 +38,7 @@ import Console from './extras/Console.js'
38
38
  import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
39
  import { dropFile } from './scripts/dropFile.js'
40
40
  import WebElement from '../element/WebElement.js'
41
+ import { selectElement } from './extras/elementSelection.js'
41
42
 
42
43
  let playwright
43
44
  let perfTiming
@@ -1779,8 +1780,7 @@ class Playwright extends Helper {
1779
1780
  if (elements.length === 0) {
1780
1781
  throw new ElementNotFound(locator, 'Element', 'was not found')
1781
1782
  }
1782
- if (this.options.strict) assertOnlyOneElement(elements, locator, this)
1783
- return elements[0]
1783
+ return selectElement(elements, locator, this)
1784
1784
  }
1785
1785
 
1786
1786
  /**
@@ -1795,8 +1795,7 @@ class Playwright extends Helper {
1795
1795
  const context = providedContext || (await this._getContext())
1796
1796
  const els = await findCheckable.call(this, locator, context)
1797
1797
  assertElementExists(els[0], locator, 'Checkbox or radio')
1798
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
1799
- return els[0]
1798
+ return selectElement(els, locator, this)
1800
1799
  }
1801
1800
 
1802
1801
  /**
@@ -2282,8 +2281,7 @@ class Playwright extends Helper {
2282
2281
  async fillField(field, value, context = null) {
2283
2282
  const els = await findFields.call(this, field, context)
2284
2283
  assertElementExists(els, field, 'Field')
2285
- if (this.options.strict) assertOnlyOneElement(els, field, this)
2286
- const el = els[0]
2284
+ const el = selectElement(els, field, this)
2287
2285
 
2288
2286
  await el.clear()
2289
2287
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
@@ -2301,9 +2299,8 @@ class Playwright extends Helper {
2301
2299
  async clearField(locator, context = null) {
2302
2300
  const els = await findFields.call(this, locator, context)
2303
2301
  assertElementExists(els, locator, 'Field to clear')
2304
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
2305
2302
 
2306
- const el = els[0]
2303
+ const el = selectElement(els, locator, this)
2307
2304
 
2308
2305
  await highlightActiveElement.call(this, el)
2309
2306
 
@@ -2318,10 +2315,10 @@ class Playwright extends Helper {
2318
2315
  async appendField(field, value, context = null) {
2319
2316
  const els = await findFields.call(this, field, context)
2320
2317
  assertElementExists(els, field, 'Field')
2321
- if (this.options.strict) assertOnlyOneElement(els, field, this)
2322
- await highlightActiveElement.call(this, els[0])
2323
- await els[0].press('End')
2324
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
2318
+ const el = selectElement(els, field, this)
2319
+ await highlightActiveElement.call(this, el)
2320
+ await el.press('End')
2321
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2325
2322
  return this._waitForAction()
2326
2323
  }
2327
2324
 
@@ -2353,22 +2350,24 @@ class Playwright extends Helper {
2353
2350
  }
2354
2351
  const els = await findFields.call(this, locator, context)
2355
2352
  if (els.length) {
2356
- const tag = await els[0].evaluate(el => el.tagName)
2357
- const type = await els[0].evaluate(el => el.type)
2353
+ const el = selectElement(els, locator, this)
2354
+ const tag = await el.evaluate(el => el.tagName)
2355
+ const type = await el.evaluate(el => el.type)
2358
2356
  if (tag === 'INPUT' && type === 'file') {
2359
- await els[0].setInputFiles(file)
2357
+ await el.setInputFiles(file)
2360
2358
  return this._waitForAction()
2361
2359
  }
2362
2360
  }
2363
2361
 
2364
2362
  const targetEls = els.length ? els : await this._locate(locator)
2365
2363
  assertElementExists(targetEls, locator, 'Element')
2364
+ const el = selectElement(targetEls, locator, this)
2366
2365
  const fileData = {
2367
2366
  base64Content: base64EncodeFile(file),
2368
2367
  fileName: path.basename(file),
2369
2368
  mimeType: getMimeType(path.basename(file)),
2370
2369
  }
2371
- await targetEls[0].evaluate(dropFile, fileData)
2370
+ await el.evaluate(dropFile, fileData)
2372
2371
  return this._waitForAction()
2373
2372
  }
2374
2373
 
@@ -2391,23 +2390,23 @@ class Playwright extends Helper {
2391
2390
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2392
2391
  const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2393
2392
  assertElementExists(els, select, 'Selectable element')
2394
- return proceedSelect.call(this, pageContext, els[0], option)
2393
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2395
2394
  }
2396
2395
 
2397
2396
  // Fuzzy: try combobox
2398
2397
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2399
2398
  const comboboxSearchCtx = contextEl || pageContext
2400
2399
  let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2401
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2400
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2402
2401
 
2403
2402
  // Fuzzy: try listbox
2404
2403
  els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2405
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2404
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2406
2405
 
2407
2406
  // Fuzzy: try native select
2408
2407
  els = await findFields.call(this, select, context)
2409
2408
  assertElementExists(els, select, 'Selectable element')
2410
- return proceedSelect.call(this, pageContext, els[0], option)
2409
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2411
2410
  }
2412
2411
 
2413
2412
  /**
@@ -4282,16 +4281,21 @@ async function proceedClick(locator, context = null, options = {}) {
4282
4281
  assertElementExists(els, locator, 'Clickable element')
4283
4282
  }
4284
4283
 
4285
- await highlightActiveElement.call(this, els[0])
4286
- if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
4284
+ const elementIndex = store.currentStep?.opts?.elementIndex
4285
+ let element
4286
+ if (elementIndex != null) {
4287
+ element = selectElement(els, locator, this)
4288
+ } else {
4289
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4290
+ element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4291
+ }
4292
+
4293
+ await highlightActiveElement.call(this, element)
4294
+ if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
4287
4295
 
4288
- /*
4289
- using the force true options itself but instead dispatching a click
4290
- */
4291
4296
  if (options.force) {
4292
- await els[0].dispatchEvent('click')
4297
+ await element.dispatchEvent('click')
4293
4298
  } else {
4294
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4295
4299
  await element.click(options)
4296
4300
  }
4297
4301
  const promises = []
@@ -4308,7 +4312,6 @@ async function findClickable(matcher, locator) {
4308
4312
 
4309
4313
  if (!matchedLocator.isFuzzy()) {
4310
4314
  const els = await findElements.call(this, matcher, matchedLocator)
4311
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4312
4315
  return els
4313
4316
  }
4314
4317
 
@@ -4317,42 +4320,27 @@ async function findClickable(matcher, locator) {
4317
4320
 
4318
4321
  try {
4319
4322
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4320
- if (els.length) {
4321
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4322
- return els
4323
- }
4323
+ if (els.length) return els
4324
4324
  } catch (err) {
4325
4325
  // getByRole not supported or failed
4326
4326
  }
4327
4327
 
4328
4328
  try {
4329
4329
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4330
- if (els.length) {
4331
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4332
- return els
4333
- }
4330
+ if (els.length) return els
4334
4331
  } catch (err) {
4335
4332
  // getByRole not supported or failed
4336
4333
  }
4337
4334
 
4338
4335
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4339
- if (els.length) {
4340
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4341
- return els
4342
- }
4336
+ if (els.length) return els
4343
4337
 
4344
4338
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4345
- if (els.length) {
4346
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4347
- return els
4348
- }
4339
+ if (els.length) return els
4349
4340
 
4350
4341
  try {
4351
4342
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4352
- if (els.length) {
4353
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4354
- return els
4355
- }
4343
+ if (els.length) return els
4356
4344
  } catch (err) {
4357
4345
  // Do nothing
4358
4346
  }
@@ -43,6 +43,7 @@ import { dropFile } from './scripts/dropFile.js'
43
43
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
44
44
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
45
45
  import WebElement from '../element/WebElement.js'
46
+ import { selectElement } from './extras/elementSelection.js'
46
47
 
47
48
  let puppeteer
48
49
 
@@ -1008,13 +1009,13 @@ class Puppeteer extends Helper {
1008
1009
  */
1009
1010
  async _locateElement(locator) {
1010
1011
  const context = await this.context
1011
- if (this.options.strict) {
1012
+ const elementIndex = store.currentStep?.opts?.elementIndex
1013
+ if (this.options.strict || elementIndex) {
1012
1014
  const elements = await findElements.call(this, context, locator)
1013
1015
  if (elements.length === 0) {
1014
1016
  throw new ElementNotFound(locator, 'Element', 'was not found')
1015
1017
  }
1016
- assertOnlyOneElement(elements, locator, this)
1017
- return elements[0]
1018
+ return selectElement(elements, locator, this)
1018
1019
  }
1019
1020
  return findElement.call(this, context, locator)
1020
1021
  }
@@ -1033,8 +1034,7 @@ class Puppeteer extends Helper {
1033
1034
  if (!els || els.length === 0) {
1034
1035
  throw new ElementNotFound(locator, 'Checkbox or radio')
1035
1036
  }
1036
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
1037
- return els[0]
1037
+ return selectElement(els, locator, this)
1038
1038
  }
1039
1039
 
1040
1040
  /**
@@ -1593,8 +1593,7 @@ class Puppeteer extends Helper {
1593
1593
  async fillField(field, value, context = null) {
1594
1594
  const els = await findVisibleFields.call(this, field, context)
1595
1595
  assertElementExists(els, field, 'Field')
1596
- if (this.options.strict) assertOnlyOneElement(els, field, this)
1597
- const el = els[0]
1596
+ const el = selectElement(els, field, this)
1598
1597
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1599
1598
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1600
1599
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1624,10 +1623,10 @@ class Puppeteer extends Helper {
1624
1623
  async appendField(field, value, context = null) {
1625
1624
  const els = await findVisibleFields.call(this, field, context)
1626
1625
  assertElementExists(els, field, 'Field')
1627
- if (this.options.strict) assertOnlyOneElement(els, field, this)
1628
- highlightActiveElement.call(this, els[0], await this._getContext())
1629
- await els[0].press('End')
1630
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
1626
+ const el = selectElement(els, field, this)
1627
+ highlightActiveElement.call(this, el, await this._getContext())
1628
+ await el.press('End')
1629
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1631
1630
  return this._waitForAction()
1632
1631
  }
1633
1632
 
@@ -1660,22 +1659,24 @@ class Puppeteer extends Helper {
1660
1659
  }
1661
1660
  const els = await findFields.call(this, locator, context)
1662
1661
  if (els.length) {
1663
- const tag = await els[0].evaluate(el => el.tagName)
1664
- const type = await els[0].evaluate(el => el.type)
1662
+ const el = selectElement(els, locator, this)
1663
+ const tag = await el.evaluate(el => el.tagName)
1664
+ const type = await el.evaluate(el => el.type)
1665
1665
  if (tag === 'INPUT' && type === 'file') {
1666
- await els[0].uploadFile(file)
1666
+ await el.uploadFile(file)
1667
1667
  return this._waitForAction()
1668
1668
  }
1669
1669
  }
1670
1670
 
1671
1671
  const targetEls = els.length ? els : await this._locate(locator)
1672
1672
  assertElementExists(targetEls, locator, 'Element')
1673
+ const el = selectElement(targetEls, locator, this)
1673
1674
  const fileData = {
1674
1675
  base64Content: base64EncodeFile(file),
1675
1676
  fileName: path.basename(file),
1676
1677
  mimeType: getMimeType(path.basename(file)),
1677
1678
  }
1678
- await targetEls[0].evaluate(dropFile, fileData)
1679
+ await el.evaluate(dropFile, fileData)
1679
1680
  return this._waitForAction()
1680
1681
  }
1681
1682
 
@@ -1698,23 +1699,23 @@ class Puppeteer extends Helper {
1698
1699
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1699
1700
  const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1700
1701
  assertElementExists(els, select, 'Selectable element')
1701
- return proceedSelect.call(this, pageContext, els[0], option)
1702
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1702
1703
  }
1703
1704
 
1704
1705
  // Fuzzy: try combobox
1705
1706
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1706
1707
  const comboboxSearchCtx = contextEl || pageContext
1707
1708
  let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1708
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
1709
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1709
1710
 
1710
1711
  // Fuzzy: try listbox
1711
1712
  els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1712
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
1713
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1713
1714
 
1714
1715
  // Fuzzy: try native select
1715
1716
  const visibleEls = await findVisibleFields.call(this, select, context)
1716
1717
  assertElementExists(visibleEls, select, 'Selectable field')
1717
- return proceedSelect.call(this, pageContext, visibleEls[0], option)
1718
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1718
1719
  }
1719
1720
 
1720
1721
  /**
@@ -3121,11 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
3121
3122
  } else {
3122
3123
  assertElementExists(els, locator, 'Clickable element')
3123
3124
  }
3124
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
3125
+ const el = selectElement(els, locator, this)
3125
3126
 
3126
- highlightActiveElement.call(this, els[0], await this._getContext())
3127
+ highlightActiveElement.call(this, el, await this._getContext())
3127
3128
 
3128
- await els[0].click(options)
3129
+ await el.click(options)
3129
3130
  const promises = []
3130
3131
  if (options.waitForNavigation) {
3131
3132
  promises.push(this.waitForNavigation())
@@ -40,6 +40,7 @@ import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElem
40
40
  import { dropFile } from './scripts/dropFile.js'
41
41
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
42
42
  import WebElement from '../element/WebElement.js'
43
+ import { selectElement } from './extras/elementSelection.js'
43
44
 
44
45
  const SHADOW = 'shadow'
45
46
  const webRoot = 'body'
@@ -1093,8 +1094,7 @@ class WebDriver extends Helper {
1093
1094
  } else {
1094
1095
  assertElementExists(res, locator, 'Clickable element')
1095
1096
  }
1096
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1097
- const elem = usingFirstElement(res)
1097
+ const elem = selectElement(res, locator, this)
1098
1098
  highlightActiveElement.call(this, elem)
1099
1099
  return this.browser[clickMethod](getElementId(elem))
1100
1100
  }
@@ -1113,8 +1113,7 @@ class WebDriver extends Helper {
1113
1113
  } else {
1114
1114
  assertElementExists(res, locator, 'Clickable element')
1115
1115
  }
1116
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1117
- const elem = usingFirstElement(res)
1116
+ const elem = selectElement(res, locator, this)
1118
1117
  highlightActiveElement.call(this, elem)
1119
1118
 
1120
1119
  return this.executeScript(el => {
@@ -1141,9 +1140,8 @@ class WebDriver extends Helper {
1141
1140
  } else {
1142
1141
  assertElementExists(res, locator, 'Clickable element')
1143
1142
  }
1144
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1145
1143
 
1146
- const elem = usingFirstElement(res)
1144
+ const elem = selectElement(res, locator, this)
1147
1145
  highlightActiveElement.call(this, elem)
1148
1146
  return elem.doubleClick()
1149
1147
  }
@@ -1162,9 +1160,8 @@ class WebDriver extends Helper {
1162
1160
  } else {
1163
1161
  assertElementExists(res, locator, 'Clickable element')
1164
1162
  }
1165
- if (this.options.strict) assertOnlyOneElement(res, locator, this)
1166
1163
 
1167
- const el = usingFirstElement(res)
1164
+ const el = selectElement(res, locator, this)
1168
1165
 
1169
1166
  await el.moveTo()
1170
1167
 
@@ -1279,8 +1276,7 @@ class WebDriver extends Helper {
1279
1276
  async fillField(field, value, context = null) {
1280
1277
  const res = await findFields.call(this, field, context)
1281
1278
  assertElementExists(res, field, 'Field')
1282
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1283
- const elem = usingFirstElement(res)
1279
+ const elem = selectElement(res, field, this)
1284
1280
  highlightActiveElement.call(this, elem)
1285
1281
  try {
1286
1282
  await elem.clearValue()
@@ -1303,8 +1299,7 @@ class WebDriver extends Helper {
1303
1299
  async appendField(field, value, context = null) {
1304
1300
  const res = await findFields.call(this, field, context)
1305
1301
  assertElementExists(res, field, 'Field')
1306
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1307
- const elem = usingFirstElement(res)
1302
+ const elem = selectElement(res, field, this)
1308
1303
  highlightActiveElement.call(this, elem)
1309
1304
  return elem.addValue(value.toString())
1310
1305
  }
@@ -1316,8 +1311,7 @@ class WebDriver extends Helper {
1316
1311
  async clearField(field, context = null) {
1317
1312
  const res = await findFields.call(this, field, context)
1318
1313
  assertElementExists(res, field, 'Field')
1319
- if (this.options.strict) assertOnlyOneElement(res, field, this)
1320
- const elem = usingFirstElement(res)
1314
+ const elem = selectElement(res, field, this)
1321
1315
  highlightActiveElement.call(this, elem)
1322
1316
  return elem.clearValue(getElementId(elem))
1323
1317
  }
@@ -1334,22 +1328,22 @@ class WebDriver extends Helper {
1334
1328
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1335
1329
  const els = await locateFn(select)
1336
1330
  assertElementExists(els, select, 'Selectable element')
1337
- return proceedSelectOption.call(this, usingFirstElement(els), option)
1331
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1338
1332
  }
1339
1333
 
1340
1334
  // Fuzzy: try combobox
1341
1335
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1342
1336
  let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1343
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1337
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1344
1338
 
1345
1339
  // Fuzzy: try listbox
1346
1340
  els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1347
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1341
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1348
1342
 
1349
1343
  // Fuzzy: try native select
1350
1344
  const res = await findFields.call(this, select, context)
1351
1345
  assertElementExists(res, select, 'Selectable field')
1352
- return proceedSelectOption.call(this, usingFirstElement(res), option)
1346
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1353
1347
  }
1354
1348
 
1355
1349
  /**
@@ -1367,7 +1361,7 @@ class WebDriver extends Helper {
1367
1361
  this.debug(`Uploading ${file}`)
1368
1362
 
1369
1363
  if (res.length) {
1370
- const el = usingFirstElement(res)
1364
+ const el = selectElement(res, locator, this)
1371
1365
  const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
1372
1366
  const type = await this.browser.execute(function (elem) { return elem.type }, el)
1373
1367
  if (tag === 'INPUT' && type === 'file') {
@@ -1385,7 +1379,7 @@ class WebDriver extends Helper {
1385
1379
 
1386
1380
  const targetRes = res.length ? res : await this._locate(locator)
1387
1381
  assertElementExists(targetRes, locator, 'Element')
1388
- const targetEl = usingFirstElement(targetRes)
1382
+ const targetEl = selectElement(targetRes, locator, this)
1389
1383
  const fileData = {
1390
1384
  base64Content: base64EncodeFile(file),
1391
1385
  fileName: path.basename(file),
@@ -1405,7 +1399,7 @@ class WebDriver extends Helper {
1405
1399
  const res = await findCheckable.call(this, field, locateFn)
1406
1400
 
1407
1401
  assertElementExists(res, field, 'Checkable')
1408
- const elem = usingFirstElement(res)
1402
+ const elem = selectElement(res, field, this)
1409
1403
  const elementId = getElementId(elem)
1410
1404
  highlightActiveElement.call(this, elem)
1411
1405
 
@@ -1426,7 +1420,7 @@ class WebDriver extends Helper {
1426
1420
  const res = await findCheckable.call(this, field, locateFn)
1427
1421
 
1428
1422
  assertElementExists(res, field, 'Checkable')
1429
- const elem = usingFirstElement(res)
1423
+ const elem = selectElement(res, field, this)
1430
1424
  const elementId = getElementId(elem)
1431
1425
  highlightActiveElement.call(this, elem)
1432
1426
 
@@ -3301,6 +3295,19 @@ function assertElementExists(res, locator, prefix, suffix) {
3301
3295
  }
3302
3296
 
3303
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
+ }
3304
3311
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3305
3312
  return els[0]
3306
3313
  }
@@ -0,0 +1,51 @@
1
+ import store from '../../store.js'
2
+ import output from '../../output.js'
3
+ import WebElement from '../../element/WebElement.js'
4
+ import MultipleElementsFound from '../errors/MultipleElementsFound.js'
5
+
6
+ function resolveElementIndex(value) {
7
+ if (value === 'first') return 1
8
+ if (value === 'last') return -1
9
+ return value
10
+ }
11
+
12
+ function selectElement(els, locator, helper) {
13
+ const rawIndex = store.currentStep?.opts?.elementIndex
14
+ const elementIndex = resolveElementIndex(rawIndex)
15
+
16
+ if (elementIndex != null) {
17
+ if (els.length === 1) return els[0]
18
+
19
+ if (!Number.isInteger(elementIndex) || elementIndex === 0) {
20
+ throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
21
+ }
22
+
23
+ let idx
24
+ if (elementIndex > 0) {
25
+ idx = elementIndex - 1
26
+ if (idx >= els.length) {
27
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
28
+ }
29
+ } else {
30
+ idx = els.length + elementIndex
31
+ if (idx < 0) {
32
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
33
+ }
34
+ }
35
+
36
+ output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
37
+ return els[idx]
38
+ }
39
+
40
+ if (helper.options.strict) {
41
+ if (els.length > 1) {
42
+ const webElements = els.map(el => new WebElement(el, helper))
43
+ throw new MultipleElementsFound(locator, webElements)
44
+ }
45
+ }
46
+
47
+ if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
48
+ return els[0]
49
+ }
50
+
51
+ export { selectElement }
@@ -1,10 +1,16 @@
1
+ /**
2
+ * @typedef {Object} StepOptions
3
+ * @property {number|'first'|'last'} [elementIndex] - Select a specific element when multiple match. 1-based positive index, negative from end, or 'first'/'last'.
4
+ * @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
5
+ */
6
+
1
7
  /**
2
8
  * StepConfig is a configuration object for a step.
3
9
  * It is used to create a new step that is a combination of other steps.
4
10
  */
5
11
  class StepConfig {
6
12
  constructor(opts = {}) {
7
- /** @member {{ opts: Record<string, any>, timeout: number|undefined, retry: number|undefined }} */
13
+ /** @member {{ opts: StepOptions, timeout: number|undefined, retry: number|undefined }} */
8
14
  this.config = {
9
15
  opts,
10
16
  timeout: undefined,
@@ -14,7 +20,7 @@ class StepConfig {
14
20
 
15
21
  /**
16
22
  * Set the options for the step.
17
- * @param {object} opts - The options for the step.
23
+ * @param {StepOptions} opts - The options for the step.
18
24
  * @returns {StepConfig} - The step configuration object.
19
25
  */
20
26
  opts(opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-rc.8",
3
+ "version": "4.0.0-rc.9",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -745,3 +745,22 @@ declare module 'codeceptjs/effects' {
745
745
  export const retryTo: RetryTo
746
746
  export const hopeThat: HopeThat
747
747
  }
748
+
749
+ declare module 'codeceptjs/steps' {
750
+ const step: {
751
+ opts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
752
+ timeout(timeout: number): CodeceptJS.StepConfig;
753
+ retry(retry: number): CodeceptJS.StepConfig;
754
+ stepOpts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
755
+ stepTimeout(timeout: number): CodeceptJS.StepConfig;
756
+ stepRetry(retry: number): CodeceptJS.StepConfig;
757
+ section(name: string): any;
758
+ endSection(): any;
759
+ Section(name: string): any;
760
+ EndSection(): any;
761
+ Given(): any;
762
+ When(): any;
763
+ Then(): any;
764
+ }
765
+ export default step
766
+ }