codeceptjs 4.0.0-rc.7 → 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.
@@ -1,12 +1,16 @@
1
1
  Moves cursor to element matched by locator.
2
2
  Extra shift can be set with offsetX and offsetY options.
3
3
 
4
+ An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent.
5
+ When the second argument is a non-number (string or locator object), it is treated as context.
6
+
4
7
  ```js
5
8
  I.moveCursorTo('.tooltip');
6
9
  I.moveCursorTo('#submit', 5,5);
10
+ I.moveCursorTo('#submit', '.container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
10
- @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
14
+ @param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator.
11
15
  @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
12
16
  @returns {void} automatically synchronized promise through #recorder
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import { simplifyHtmlElement } from '../html.js'
2
3
 
3
4
  /**
4
5
  * Unified WebElement class that wraps native element instances from different helpers
@@ -306,6 +307,57 @@ class WebElement {
306
307
  * @returns {string} Normalized CSS selector
307
308
  * @private
308
309
  */
310
+ async toAbsoluteXPath() {
311
+ const xpathFn = (el) => {
312
+ const parts = []
313
+ let current = el
314
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
315
+ let index = 0
316
+ let sibling = current.previousSibling
317
+ while (sibling) {
318
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
319
+ index++
320
+ }
321
+ sibling = sibling.previousSibling
322
+ }
323
+ const tagName = current.tagName.toLowerCase()
324
+ const pathIndex = index > 0 ? `[${index + 1}]` : ''
325
+ parts.unshift(`${tagName}${pathIndex}`)
326
+ current = current.parentElement
327
+ }
328
+ return '//' + parts.join('/')
329
+ }
330
+
331
+ switch (this.helperType) {
332
+ case 'playwright':
333
+ return this.element.evaluate(xpathFn)
334
+ case 'puppeteer':
335
+ return this.element.evaluate(xpathFn)
336
+ case 'webdriver':
337
+ return this.helper.browser.execute(xpathFn, this.element)
338
+ default:
339
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
340
+ }
341
+ }
342
+
343
+ async toOuterHTML() {
344
+ switch (this.helperType) {
345
+ case 'playwright':
346
+ return this.element.evaluate(el => el.outerHTML)
347
+ case 'puppeteer':
348
+ return this.element.evaluate(el => el.outerHTML)
349
+ case 'webdriver':
350
+ return this.helper.browser.execute(el => el.outerHTML, this.element)
351
+ default:
352
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
353
+ }
354
+ }
355
+
356
+ async toSimplifiedHTML(maxLength = 300) {
357
+ const outerHTML = await this.toOuterHTML()
358
+ return simplifyHtmlElement(outerHTML, maxLength)
359
+ }
360
+
309
361
  _normalizeLocator(locator) {
310
362
  if (typeof locator === 'string') {
311
363
  return locator
@@ -36,7 +36,9 @@ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefu
36
36
  import Popup from './extras/Popup.js'
37
37
  import Console from './extras/Console.js'
38
38
  import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
+ import { dropFile } from './scripts/dropFile.js'
39
40
  import WebElement from '../element/WebElement.js'
41
+ import { selectElement } from './extras/elementSelection.js'
40
42
 
41
43
  let playwright
42
44
  let perfTiming
@@ -1494,8 +1496,23 @@ class Playwright extends Helper {
1494
1496
  *
1495
1497
  */
1496
1498
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1497
- const el = await this._locateElement(locator)
1498
- assertElementExists(el, locator)
1499
+ let context = null
1500
+ if (typeof offsetX !== 'number') {
1501
+ context = offsetX
1502
+ offsetX = 0
1503
+ }
1504
+
1505
+ let el
1506
+ if (context) {
1507
+ const contextEls = await this._locate(context)
1508
+ assertElementExists(contextEls, context, 'Context element')
1509
+ el = await findElements.call(this, contextEls[0], locator)
1510
+ assertElementExists(el, locator)
1511
+ el = el[0]
1512
+ } else {
1513
+ el = await this._locateElement(locator)
1514
+ assertElementExists(el, locator)
1515
+ }
1499
1516
 
1500
1517
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
1501
1518
  const { x, y } = await clickablePoint(el)
@@ -1763,8 +1780,7 @@ class Playwright extends Helper {
1763
1780
  if (elements.length === 0) {
1764
1781
  throw new ElementNotFound(locator, 'Element', 'was not found')
1765
1782
  }
1766
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1767
- return elements[0]
1783
+ return selectElement(elements, locator, this)
1768
1784
  }
1769
1785
 
1770
1786
  /**
@@ -1779,8 +1795,7 @@ class Playwright extends Helper {
1779
1795
  const context = providedContext || (await this._getContext())
1780
1796
  const els = await findCheckable.call(this, locator, context)
1781
1797
  assertElementExists(els[0], locator, 'Checkbox or radio')
1782
- if (this.options.strict) assertOnlyOneElement(els, locator)
1783
- return els[0]
1798
+ return selectElement(els, locator, this)
1784
1799
  }
1785
1800
 
1786
1801
  /**
@@ -2266,8 +2281,7 @@ class Playwright extends Helper {
2266
2281
  async fillField(field, value, context = null) {
2267
2282
  const els = await findFields.call(this, field, context)
2268
2283
  assertElementExists(els, field, 'Field')
2269
- if (this.options.strict) assertOnlyOneElement(els, field)
2270
- const el = els[0]
2284
+ const el = selectElement(els, field, this)
2271
2285
 
2272
2286
  await el.clear()
2273
2287
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
@@ -2285,9 +2299,8 @@ class Playwright extends Helper {
2285
2299
  async clearField(locator, context = null) {
2286
2300
  const els = await findFields.call(this, locator, context)
2287
2301
  assertElementExists(els, locator, 'Field to clear')
2288
- if (this.options.strict) assertOnlyOneElement(els, locator)
2289
2302
 
2290
- const el = els[0]
2303
+ const el = selectElement(els, locator, this)
2291
2304
 
2292
2305
  await highlightActiveElement.call(this, el)
2293
2306
 
@@ -2302,10 +2315,10 @@ class Playwright extends Helper {
2302
2315
  async appendField(field, value, context = null) {
2303
2316
  const els = await findFields.call(this, field, context)
2304
2317
  assertElementExists(els, field, 'Field')
2305
- if (this.options.strict) assertOnlyOneElement(els, field)
2306
- await highlightActiveElement.call(this, els[0])
2307
- await els[0].press('End')
2308
- 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 })
2309
2322
  return this._waitForAction()
2310
2323
  }
2311
2324
 
@@ -2337,30 +2350,24 @@ class Playwright extends Helper {
2337
2350
  }
2338
2351
  const els = await findFields.call(this, locator, context)
2339
2352
  if (els.length) {
2340
- const tag = await els[0].evaluate(el => el.tagName)
2341
- 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)
2342
2356
  if (tag === 'INPUT' && type === 'file') {
2343
- await els[0].setInputFiles(file)
2357
+ await el.setInputFiles(file)
2344
2358
  return this._waitForAction()
2345
2359
  }
2346
2360
  }
2347
2361
 
2348
2362
  const targetEls = els.length ? els : await this._locate(locator)
2349
2363
  assertElementExists(targetEls, locator, 'Element')
2350
- const base64Content = base64EncodeFile(file)
2351
- const fileName = path.basename(file)
2352
- const mimeType = getMimeType(fileName)
2353
- await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
2354
- const binaryStr = atob(base64Content)
2355
- const bytes = new Uint8Array(binaryStr.length)
2356
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
2357
- const fileObj = new File([bytes], fileName, { type: mimeType })
2358
- const dataTransfer = new DataTransfer()
2359
- dataTransfer.items.add(fileObj)
2360
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
2361
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
2362
- el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
2363
- }, { base64Content, fileName, mimeType })
2364
+ const el = selectElement(targetEls, locator, this)
2365
+ const fileData = {
2366
+ base64Content: base64EncodeFile(file),
2367
+ fileName: path.basename(file),
2368
+ mimeType: getMimeType(path.basename(file)),
2369
+ }
2370
+ await el.evaluate(dropFile, fileData)
2364
2371
  return this._waitForAction()
2365
2372
  }
2366
2373
 
@@ -2383,23 +2390,23 @@ class Playwright extends Helper {
2383
2390
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2384
2391
  const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2385
2392
  assertElementExists(els, select, 'Selectable element')
2386
- return proceedSelect.call(this, pageContext, els[0], option)
2393
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2387
2394
  }
2388
2395
 
2389
2396
  // Fuzzy: try combobox
2390
2397
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2391
2398
  const comboboxSearchCtx = contextEl || pageContext
2392
2399
  let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2393
- 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)
2394
2401
 
2395
2402
  // Fuzzy: try listbox
2396
2403
  els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2397
- 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)
2398
2405
 
2399
2406
  // Fuzzy: try native select
2400
2407
  els = await findFields.call(this, select, context)
2401
2408
  assertElementExists(els, select, 'Selectable element')
2402
- return proceedSelect.call(this, pageContext, els[0], option)
2409
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2403
2410
  }
2404
2411
 
2405
2412
  /**
@@ -4274,16 +4281,21 @@ async function proceedClick(locator, context = null, options = {}) {
4274
4281
  assertElementExists(els, locator, 'Clickable element')
4275
4282
  }
4276
4283
 
4277
- await highlightActiveElement.call(this, els[0])
4278
- 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))
4279
4295
 
4280
- /*
4281
- using the force true options itself but instead dispatching a click
4282
- */
4283
4296
  if (options.force) {
4284
- await els[0].dispatchEvent('click')
4297
+ await element.dispatchEvent('click')
4285
4298
  } else {
4286
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4287
4299
  await element.click(options)
4288
4300
  }
4289
4301
  const promises = []
@@ -4300,7 +4312,6 @@ async function findClickable(matcher, locator) {
4300
4312
 
4301
4313
  if (!matchedLocator.isFuzzy()) {
4302
4314
  const els = await findElements.call(this, matcher, matchedLocator)
4303
- if (this.options.strict) assertOnlyOneElement(els, locator)
4304
4315
  return els
4305
4316
  }
4306
4317
 
@@ -4309,42 +4320,27 @@ async function findClickable(matcher, locator) {
4309
4320
 
4310
4321
  try {
4311
4322
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4312
- if (els.length) {
4313
- if (this.options.strict) assertOnlyOneElement(els, locator)
4314
- return els
4315
- }
4323
+ if (els.length) return els
4316
4324
  } catch (err) {
4317
4325
  // getByRole not supported or failed
4318
4326
  }
4319
4327
 
4320
4328
  try {
4321
4329
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4322
- if (els.length) {
4323
- if (this.options.strict) assertOnlyOneElement(els, locator)
4324
- return els
4325
- }
4330
+ if (els.length) return els
4326
4331
  } catch (err) {
4327
4332
  // getByRole not supported or failed
4328
4333
  }
4329
4334
 
4330
4335
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4331
- if (els.length) {
4332
- if (this.options.strict) assertOnlyOneElement(els, locator)
4333
- return els
4334
- }
4336
+ if (els.length) return els
4335
4337
 
4336
4338
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4337
- if (els.length) {
4338
- if (this.options.strict) assertOnlyOneElement(els, locator)
4339
- return els
4340
- }
4339
+ if (els.length) return els
4341
4340
 
4342
4341
  try {
4343
4342
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4344
- if (els.length) {
4345
- if (this.options.strict) assertOnlyOneElement(els, locator)
4346
- return els
4347
- }
4343
+ if (els.length) return els
4348
4344
  } catch (err) {
4349
4345
  // Do nothing
4350
4346
  }
@@ -4619,9 +4615,10 @@ function assertElementExists(res, locator, prefix, suffix) {
4619
4615
  }
4620
4616
  }
4621
4617
 
4622
- function assertOnlyOneElement(elements, locator) {
4618
+ function assertOnlyOneElement(elements, locator, helper) {
4623
4619
  if (elements.length > 1) {
4624
- throw new MultipleElementsFound(locator, elements)
4620
+ const webElements = elements.map(el => new WebElement(el, helper))
4621
+ throw new MultipleElementsFound(locator, webElements)
4625
4622
  }
4626
4623
  }
4627
4624
 
@@ -33,14 +33,17 @@ import {
33
33
  } from '../utils.js'
34
34
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
35
35
  import ElementNotFound from './errors/ElementNotFound.js'
36
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
36
37
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
37
38
  import Popup from './extras/Popup.js'
38
39
  import Console from './extras/Console.js'
39
40
  import { highlightElement } from './scripts/highlightElement.js'
40
41
  import { blurElement } from './scripts/blurElement.js'
42
+ import { dropFile } from './scripts/dropFile.js'
41
43
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
42
44
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
43
45
  import WebElement from '../element/WebElement.js'
46
+ import { selectElement } from './extras/elementSelection.js'
44
47
 
45
48
  let puppeteer
46
49
 
@@ -270,6 +273,7 @@ class Puppeteer extends Helper {
270
273
  show: false,
271
274
  defaultPopupAction: 'accept',
272
275
  highlightElement: false,
276
+ strict: false,
273
277
  }
274
278
 
275
279
  return Object.assign(defaults, config)
@@ -818,9 +822,26 @@ class Puppeteer extends Helper {
818
822
  * {{ react }}
819
823
  */
820
824
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
821
- const el = await this._locateElement(locator)
822
- if (!el) {
823
- throw new ElementNotFound(locator, 'Element to move cursor to')
825
+ let context = null
826
+ if (typeof offsetX !== 'number') {
827
+ context = offsetX
828
+ offsetX = 0
829
+ }
830
+
831
+ let el
832
+ if (context) {
833
+ const contextEls = await findElements.call(this, this.page, context)
834
+ assertElementExists(contextEls, context, 'Context element')
835
+ const els = await findElements.call(this, contextEls[0], locator)
836
+ if (!els || els.length === 0) {
837
+ throw new ElementNotFound(locator, 'Element to move cursor to')
838
+ }
839
+ el = els[0]
840
+ } else {
841
+ el = await this._locateElement(locator)
842
+ if (!el) {
843
+ throw new ElementNotFound(locator, 'Element to move cursor to')
844
+ }
824
845
  }
825
846
 
826
847
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -988,6 +1009,14 @@ class Puppeteer extends Helper {
988
1009
  */
989
1010
  async _locateElement(locator) {
990
1011
  const context = await this.context
1012
+ const elementIndex = store.currentStep?.opts?.elementIndex
1013
+ if (this.options.strict || elementIndex) {
1014
+ const elements = await findElements.call(this, context, locator)
1015
+ if (elements.length === 0) {
1016
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1017
+ }
1018
+ return selectElement(elements, locator, this)
1019
+ }
991
1020
  return findElement.call(this, context, locator)
992
1021
  }
993
1022
 
@@ -1005,7 +1034,7 @@ class Puppeteer extends Helper {
1005
1034
  if (!els || els.length === 0) {
1006
1035
  throw new ElementNotFound(locator, 'Checkbox or radio')
1007
1036
  }
1008
- return els[0]
1037
+ return selectElement(els, locator, this)
1009
1038
  }
1010
1039
 
1011
1040
  /**
@@ -1564,7 +1593,7 @@ class Puppeteer extends Helper {
1564
1593
  async fillField(field, value, context = null) {
1565
1594
  const els = await findVisibleFields.call(this, field, context)
1566
1595
  assertElementExists(els, field, 'Field')
1567
- const el = els[0]
1596
+ const el = selectElement(els, field, this)
1568
1597
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1569
1598
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1570
1599
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1594,9 +1623,10 @@ class Puppeteer extends Helper {
1594
1623
  async appendField(field, value, context = null) {
1595
1624
  const els = await findVisibleFields.call(this, field, context)
1596
1625
  assertElementExists(els, field, 'Field')
1597
- highlightActiveElement.call(this, els[0], await this._getContext())
1598
- await els[0].press('End')
1599
- 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 })
1600
1630
  return this._waitForAction()
1601
1631
  }
1602
1632
 
@@ -1629,30 +1659,24 @@ class Puppeteer extends Helper {
1629
1659
  }
1630
1660
  const els = await findFields.call(this, locator, context)
1631
1661
  if (els.length) {
1632
- const tag = await els[0].evaluate(el => el.tagName)
1633
- 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)
1634
1665
  if (tag === 'INPUT' && type === 'file') {
1635
- await els[0].uploadFile(file)
1666
+ await el.uploadFile(file)
1636
1667
  return this._waitForAction()
1637
1668
  }
1638
1669
  }
1639
1670
 
1640
1671
  const targetEls = els.length ? els : await this._locate(locator)
1641
1672
  assertElementExists(targetEls, locator, 'Element')
1642
- const base64Content = base64EncodeFile(file)
1643
- const fileName = path.basename(file)
1644
- const mimeType = getMimeType(fileName)
1645
- await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
1646
- const binaryStr = atob(base64Content)
1647
- const bytes = new Uint8Array(binaryStr.length)
1648
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
1649
- const fileObj = new File([bytes], fileName, { type: mimeType })
1650
- const dataTransfer = new DataTransfer()
1651
- dataTransfer.items.add(fileObj)
1652
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
1653
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
1654
- el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
1655
- }, { base64Content, fileName, mimeType })
1673
+ const el = selectElement(targetEls, locator, this)
1674
+ const fileData = {
1675
+ base64Content: base64EncodeFile(file),
1676
+ fileName: path.basename(file),
1677
+ mimeType: getMimeType(path.basename(file)),
1678
+ }
1679
+ await el.evaluate(dropFile, fileData)
1656
1680
  return this._waitForAction()
1657
1681
  }
1658
1682
 
@@ -1675,23 +1699,23 @@ class Puppeteer extends Helper {
1675
1699
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1676
1700
  const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1677
1701
  assertElementExists(els, select, 'Selectable element')
1678
- return proceedSelect.call(this, pageContext, els[0], option)
1702
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1679
1703
  }
1680
1704
 
1681
1705
  // Fuzzy: try combobox
1682
1706
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1683
1707
  const comboboxSearchCtx = contextEl || pageContext
1684
1708
  let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1685
- 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)
1686
1710
 
1687
1711
  // Fuzzy: try listbox
1688
1712
  els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1689
- 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)
1690
1714
 
1691
1715
  // Fuzzy: try native select
1692
1716
  const visibleEls = await findVisibleFields.call(this, select, context)
1693
1717
  assertElementExists(visibleEls, select, 'Selectable field')
1694
- return proceedSelect.call(this, pageContext, visibleEls[0], option)
1718
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1695
1719
  }
1696
1720
 
1697
1721
  /**
@@ -3098,10 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
3098
3122
  } else {
3099
3123
  assertElementExists(els, locator, 'Clickable element')
3100
3124
  }
3125
+ const el = selectElement(els, locator, this)
3101
3126
 
3102
- highlightActiveElement.call(this, els[0], await this._getContext())
3127
+ highlightActiveElement.call(this, el, await this._getContext())
3103
3128
 
3104
- await els[0].click(options)
3129
+ await el.click(options)
3105
3130
  const promises = []
3106
3131
  if (options.waitForNavigation) {
3107
3132
  promises.push(this.waitForNavigation())
@@ -3425,6 +3450,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3425
3450
  }
3426
3451
  }
3427
3452
 
3453
+ function assertOnlyOneElement(elements, locator, helper) {
3454
+ if (elements.length > 1) {
3455
+ const webElements = elements.map(el => new WebElement(el, helper))
3456
+ throw new MultipleElementsFound(locator, webElements)
3457
+ }
3458
+ }
3459
+
3428
3460
  function $XPath(element, selector) {
3429
3461
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3430
3462
  const res = []
@@ -30,14 +30,17 @@ import {
30
30
  } from '../utils.js'
31
31
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
32
32
  import ElementNotFound from './errors/ElementNotFound.js'
33
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
33
34
  import ConnectionRefused from './errors/ConnectionRefused.js'
34
35
  import Locator from '../locator.js'
35
36
  import { highlightElement } from './scripts/highlightElement.js'
36
37
  import { focusElement } from './scripts/focusElement.js'
37
38
  import { blurElement } from './scripts/blurElement.js'
38
39
  import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
40
+ import { dropFile } from './scripts/dropFile.js'
39
41
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
40
42
  import WebElement from '../element/WebElement.js'
43
+ import { selectElement } from './extras/elementSelection.js'
41
44
 
42
45
  const SHADOW = 'shadow'
43
46
  const webRoot = 'body'
@@ -503,6 +506,7 @@ class WebDriver extends Helper {
503
506
  keepBrowserState: false,
504
507
  deprecationWarnings: false,
505
508
  highlightElement: false,
509
+ strict: false,
506
510
  }
507
511
 
508
512
  // override defaults with config
@@ -1090,7 +1094,7 @@ class WebDriver extends Helper {
1090
1094
  } else {
1091
1095
  assertElementExists(res, locator, 'Clickable element')
1092
1096
  }
1093
- const elem = usingFirstElement(res)
1097
+ const elem = selectElement(res, locator, this)
1094
1098
  highlightActiveElement.call(this, elem)
1095
1099
  return this.browser[clickMethod](getElementId(elem))
1096
1100
  }
@@ -1109,7 +1113,7 @@ class WebDriver extends Helper {
1109
1113
  } else {
1110
1114
  assertElementExists(res, locator, 'Clickable element')
1111
1115
  }
1112
- const elem = usingFirstElement(res)
1116
+ const elem = selectElement(res, locator, this)
1113
1117
  highlightActiveElement.call(this, elem)
1114
1118
 
1115
1119
  return this.executeScript(el => {
@@ -1137,7 +1141,7 @@ class WebDriver extends Helper {
1137
1141
  assertElementExists(res, locator, 'Clickable element')
1138
1142
  }
1139
1143
 
1140
- const elem = usingFirstElement(res)
1144
+ const elem = selectElement(res, locator, this)
1141
1145
  highlightActiveElement.call(this, elem)
1142
1146
  return elem.doubleClick()
1143
1147
  }
@@ -1157,7 +1161,7 @@ class WebDriver extends Helper {
1157
1161
  assertElementExists(res, locator, 'Clickable element')
1158
1162
  }
1159
1163
 
1160
- const el = usingFirstElement(res)
1164
+ const el = selectElement(res, locator, this)
1161
1165
 
1162
1166
  await el.moveTo()
1163
1167
 
@@ -1272,7 +1276,7 @@ class WebDriver extends Helper {
1272
1276
  async fillField(field, value, context = null) {
1273
1277
  const res = await findFields.call(this, field, context)
1274
1278
  assertElementExists(res, field, 'Field')
1275
- const elem = usingFirstElement(res)
1279
+ const elem = selectElement(res, field, this)
1276
1280
  highlightActiveElement.call(this, elem)
1277
1281
  try {
1278
1282
  await elem.clearValue()
@@ -1295,7 +1299,7 @@ class WebDriver extends Helper {
1295
1299
  async appendField(field, value, context = null) {
1296
1300
  const res = await findFields.call(this, field, context)
1297
1301
  assertElementExists(res, field, 'Field')
1298
- const elem = usingFirstElement(res)
1302
+ const elem = selectElement(res, field, this)
1299
1303
  highlightActiveElement.call(this, elem)
1300
1304
  return elem.addValue(value.toString())
1301
1305
  }
@@ -1307,7 +1311,7 @@ class WebDriver extends Helper {
1307
1311
  async clearField(field, context = null) {
1308
1312
  const res = await findFields.call(this, field, context)
1309
1313
  assertElementExists(res, field, 'Field')
1310
- const elem = usingFirstElement(res)
1314
+ const elem = selectElement(res, field, this)
1311
1315
  highlightActiveElement.call(this, elem)
1312
1316
  return elem.clearValue(getElementId(elem))
1313
1317
  }
@@ -1324,22 +1328,22 @@ class WebDriver extends Helper {
1324
1328
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1325
1329
  const els = await locateFn(select)
1326
1330
  assertElementExists(els, select, 'Selectable element')
1327
- return proceedSelectOption.call(this, usingFirstElement(els), option)
1331
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1328
1332
  }
1329
1333
 
1330
1334
  // Fuzzy: try combobox
1331
1335
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1332
1336
  let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1333
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1337
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1334
1338
 
1335
1339
  // Fuzzy: try listbox
1336
1340
  els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1337
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1341
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1338
1342
 
1339
1343
  // Fuzzy: try native select
1340
1344
  const res = await findFields.call(this, select, context)
1341
1345
  assertElementExists(res, select, 'Selectable field')
1342
- return proceedSelectOption.call(this, usingFirstElement(res), option)
1346
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1343
1347
  }
1344
1348
 
1345
1349
  /**
@@ -1357,7 +1361,7 @@ class WebDriver extends Helper {
1357
1361
  this.debug(`Uploading ${file}`)
1358
1362
 
1359
1363
  if (res.length) {
1360
- const el = usingFirstElement(res)
1364
+ const el = selectElement(res, locator, this)
1361
1365
  const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
1362
1366
  const type = await this.browser.execute(function (elem) { return elem.type }, el)
1363
1367
  if (tag === 'INPUT' && type === 'file') {
@@ -1375,21 +1379,13 @@ class WebDriver extends Helper {
1375
1379
 
1376
1380
  const targetRes = res.length ? res : await this._locate(locator)
1377
1381
  assertElementExists(targetRes, locator, 'Element')
1378
- const targetEl = usingFirstElement(targetRes)
1379
- const base64Content = base64EncodeFile(file)
1380
- const fileName = path.basename(file)
1381
- const mimeType = getMimeType(fileName)
1382
- return this.browser.execute(function (el, data) {
1383
- var binaryStr = atob(data.base64Content)
1384
- var bytes = new Uint8Array(binaryStr.length)
1385
- for (var i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
1386
- var fileObj = new File([bytes], data.fileName, { type: data.mimeType })
1387
- var dataTransfer = new DataTransfer()
1388
- dataTransfer.items.add(fileObj)
1389
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer: dataTransfer, bubbles: true }))
1390
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer: dataTransfer, bubbles: true }))
1391
- el.dispatchEvent(new DragEvent('drop', { dataTransfer: dataTransfer, bubbles: true }))
1392
- }, targetEl, { base64Content, fileName, mimeType })
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)
1393
1389
  }
1394
1390
 
1395
1391
  /**
@@ -1403,7 +1399,7 @@ class WebDriver extends Helper {
1403
1399
  const res = await findCheckable.call(this, field, locateFn)
1404
1400
 
1405
1401
  assertElementExists(res, field, 'Checkable')
1406
- const elem = usingFirstElement(res)
1402
+ const elem = selectElement(res, field, this)
1407
1403
  const elementId = getElementId(elem)
1408
1404
  highlightActiveElement.call(this, elem)
1409
1405
 
@@ -1424,7 +1420,7 @@ class WebDriver extends Helper {
1424
1420
  const res = await findCheckable.call(this, field, locateFn)
1425
1421
 
1426
1422
  assertElementExists(res, field, 'Checkable')
1427
- const elem = usingFirstElement(res)
1423
+ const elem = selectElement(res, field, this)
1428
1424
  const elementId = getElementId(elem)
1429
1425
  highlightActiveElement.call(this, elem)
1430
1426
 
@@ -1974,8 +1970,22 @@ class WebDriver extends Helper {
1974
1970
  * {{> moveCursorTo }}
1975
1971
  */
1976
1972
  async moveCursorTo(locator, xOffset, yOffset) {
1977
- const res = await this._locate(withStrictLocator(locator), true)
1978
- 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
+ }
1979
1989
  const elem = usingFirstElement(res)
1980
1990
  try {
1981
1991
  await elem.moveTo({ xOffset, yOffset })
@@ -3285,10 +3295,30 @@ function assertElementExists(res, locator, prefix, suffix) {
3285
3295
  }
3286
3296
 
3287
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
+ }
3288
3311
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3289
3312
  return els[0]
3290
3313
  }
3291
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
+
3292
3322
  function getElementId(el) {
3293
3323
  // W3C WebDriver web element identifier
3294
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
@@ -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 }
@@ -0,0 +1,11 @@
1
+ export const dropFile = (el, { base64Content, fileName, mimeType }) => {
2
+ const binaryStr = atob(base64Content)
3
+ const bytes = new Uint8Array(binaryStr.length)
4
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
5
+ const fileObj = new File([bytes], fileName, { type: mimeType })
6
+ const dataTransfer = new DataTransfer()
7
+ dataTransfer.items.add(fileObj)
8
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
9
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
10
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
11
+ }
package/lib/html.js CHANGED
@@ -245,4 +245,17 @@ function splitByChunks(text, chunkSize) {
245
245
  return chunks.map(chunk => chunk.trim())
246
246
  }
247
247
 
248
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml }
248
+ function simplifyHtmlElement(html, maxLength = 300) {
249
+ try {
250
+ html = removeNonInteractiveElements(html)
251
+ html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
252
+ } catch (e) {
253
+ // keep raw html if minification fails
254
+ }
255
+ if (html.length > maxLength) {
256
+ html = html.slice(0, maxLength) + '...'
257
+ }
258
+ return html
259
+ }
260
+
261
+ export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement }
package/lib/mocha/cli.js CHANGED
@@ -202,6 +202,16 @@ class Cli extends Base {
202
202
 
203
203
  // failures
204
204
  if (stats.failures) {
205
+ for (const test of this.failures) {
206
+ if (test.err && typeof test.err.fetchDetails === 'function') {
207
+ try {
208
+ await test.err.fetchDetails()
209
+ } catch (e) {
210
+ // ignore fetch errors
211
+ }
212
+ }
213
+ }
214
+
205
215
  // append step traces
206
216
  this.failures = this.failures.map(test => {
207
217
  // we will change the stack trace, so we need to clone the test
@@ -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.7",
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
+ }