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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +610 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/moveCursorTo.mustache +5 -1
  12. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  13. package/docs/webapi/seeElement.mustache +4 -0
  14. package/docs/webapi/seeInField.mustache +5 -0
  15. package/docs/webapi/selectOption.mustache +5 -0
  16. package/docs/webapi/uncheckOption.mustache +1 -1
  17. package/lib/codecept.js +20 -17
  18. package/lib/command/init.js +0 -3
  19. package/lib/command/run-workers.js +1 -0
  20. package/lib/container.js +19 -4
  21. package/lib/element/WebElement.js +81 -2
  22. package/lib/els.js +12 -6
  23. package/lib/helper/Appium.js +8 -8
  24. package/lib/helper/Playwright.js +219 -137
  25. package/lib/helper/Puppeteer.js +207 -69
  26. package/lib/helper/WebDriver.js +179 -64
  27. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  28. package/lib/helper/extras/elementSelection.js +58 -0
  29. package/lib/helper/scripts/dropFile.js +11 -0
  30. package/lib/html.js +14 -1
  31. package/lib/listener/globalRetry.js +32 -6
  32. package/lib/mocha/cli.js +10 -0
  33. package/lib/plugin/aiTrace.js +464 -0
  34. package/lib/plugin/retryFailedStep.js +28 -19
  35. package/lib/plugin/stepByStepReport.js +5 -1
  36. package/lib/step/config.js +15 -2
  37. package/lib/step/record.js +1 -1
  38. package/lib/utils.js +48 -0
  39. package/lib/workers.js +49 -7
  40. package/package.json +5 -3
  41. package/typings/index.d.ts +19 -0
  42. package/lib/listener/enhancedGlobalRetry.js +0 -110
  43. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  44. package/lib/plugin/htmlReporter.js +0 -3648
  45. package/lib/retryCoordinator.js +0 -207
  46. package/typings/promiseBasedTypes.d.ts +0 -9469
  47. package/typings/types.d.ts +0 -11402
@@ -23,7 +23,11 @@ import {
23
23
  clearString,
24
24
  requireWithFallback,
25
25
  normalizeSpacesInString,
26
+ normalizePath,
27
+ resolveUrl,
26
28
  relativeDir,
29
+ getMimeType,
30
+ base64EncodeFile,
27
31
  } from '../utils.js'
28
32
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
29
33
  import ElementNotFound from './errors/ElementNotFound.js'
@@ -32,7 +36,9 @@ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefu
32
36
  import Popup from './extras/Popup.js'
33
37
  import Console from './extras/Console.js'
34
38
  import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
+ import { dropFile } from './scripts/dropFile.js'
35
40
  import WebElement from '../element/WebElement.js'
41
+ import { selectElement } from './extras/elementSelection.js'
36
42
 
37
43
  let playwright
38
44
  let perfTiming
@@ -1490,8 +1496,23 @@ class Playwright extends Helper {
1490
1496
  *
1491
1497
  */
1492
1498
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1493
- const el = await this._locateElement(locator)
1494
- 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
+ }
1495
1516
 
1496
1517
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
1497
1518
  const { x, y } = await clickablePoint(el)
@@ -1759,8 +1780,7 @@ class Playwright extends Helper {
1759
1780
  if (elements.length === 0) {
1760
1781
  throw new ElementNotFound(locator, 'Element', 'was not found')
1761
1782
  }
1762
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1763
- return elements[0]
1783
+ return selectElement(elements, locator, this)
1764
1784
  }
1765
1785
 
1766
1786
  /**
@@ -1775,8 +1795,7 @@ class Playwright extends Helper {
1775
1795
  const context = providedContext || (await this._getContext())
1776
1796
  const els = await findCheckable.call(this, locator, context)
1777
1797
  assertElementExists(els[0], locator, 'Checkbox or radio')
1778
- if (this.options.strict) assertOnlyOneElement(els, locator)
1779
- return els[0]
1798
+ return selectElement(els, locator, this)
1780
1799
  }
1781
1800
 
1782
1801
  /**
@@ -1944,8 +1963,15 @@ class Playwright extends Helper {
1944
1963
  * {{> seeElement }}
1945
1964
  *
1946
1965
  */
1947
- async seeElement(locator) {
1948
- let els = await this._locate(locator)
1966
+ async seeElement(locator, context = null) {
1967
+ let els
1968
+ if (context) {
1969
+ const contextEls = await this._locate(context)
1970
+ assertElementExists(contextEls, context, 'Context element')
1971
+ els = await findElements.call(this, contextEls[0], locator)
1972
+ } else {
1973
+ els = await this._locate(locator)
1974
+ }
1949
1975
  els = await Promise.all(els.map(el => el.isVisible()))
1950
1976
  try {
1951
1977
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
@@ -1958,8 +1984,15 @@ class Playwright extends Helper {
1958
1984
  * {{> dontSeeElement }}
1959
1985
  *
1960
1986
  */
1961
- async dontSeeElement(locator) {
1962
- let els = await this._locate(locator)
1987
+ async dontSeeElement(locator, context = null) {
1988
+ let els
1989
+ if (context) {
1990
+ const contextEls = await this._locate(context)
1991
+ assertElementExists(contextEls, context, 'Context element')
1992
+ els = await findElements.call(this, contextEls[0], locator)
1993
+ } else {
1994
+ els = await this._locate(locator)
1995
+ }
1963
1996
  els = await Promise.all(els.map(el => el.isVisible()))
1964
1997
  try {
1965
1998
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
@@ -2245,11 +2278,10 @@ class Playwright extends Helper {
2245
2278
  * {{> fillField }}
2246
2279
  *
2247
2280
  */
2248
- async fillField(field, value) {
2249
- const els = await findFields.call(this, field)
2281
+ async fillField(field, value, context = null) {
2282
+ const els = await findFields.call(this, field, context)
2250
2283
  assertElementExists(els, field, 'Field')
2251
- if (this.options.strict) assertOnlyOneElement(els, field)
2252
- const el = els[0]
2284
+ const el = selectElement(els, field, this)
2253
2285
 
2254
2286
  await el.clear()
2255
2287
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
@@ -2262,28 +2294,13 @@ class Playwright extends Helper {
2262
2294
  }
2263
2295
 
2264
2296
  /**
2265
- * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
2266
- *
2267
- *
2268
- * Examples:
2269
- *
2270
- * ```js
2271
- * I.clearField('.text-area')
2272
- *
2273
- * // if this doesn't work use force option
2274
- * I.clearField('#submit', { force: true })
2275
- * ```
2276
- * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
2277
- *
2278
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
2279
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
2297
+ * {{> clearField }}
2280
2298
  */
2281
- async clearField(locator, options = {}) {
2282
- const els = await findFields.call(this, locator)
2299
+ async clearField(locator, context = null) {
2300
+ const els = await findFields.call(this, locator, context)
2283
2301
  assertElementExists(els, locator, 'Field to clear')
2284
- if (this.options.strict) assertOnlyOneElement(els, locator)
2285
2302
 
2286
- const el = els[0]
2303
+ const el = selectElement(els, locator, this)
2287
2304
 
2288
2305
  await highlightActiveElement.call(this, el)
2289
2306
 
@@ -2295,76 +2312,101 @@ class Playwright extends Helper {
2295
2312
  /**
2296
2313
  * {{> appendField }}
2297
2314
  */
2298
- async appendField(field, value) {
2299
- const els = await findFields.call(this, field)
2315
+ async appendField(field, value, context = null) {
2316
+ const els = await findFields.call(this, field, context)
2300
2317
  assertElementExists(els, field, 'Field')
2301
- if (this.options.strict) assertOnlyOneElement(els, field)
2302
- await highlightActiveElement.call(this, els[0])
2303
- await els[0].press('End')
2304
- 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 })
2305
2322
  return this._waitForAction()
2306
2323
  }
2307
2324
 
2308
2325
  /**
2309
2326
  * {{> seeInField }}
2310
2327
  */
2311
- async seeInField(field, value) {
2328
+ async seeInField(field, value, context = null) {
2312
2329
  const _value = typeof value === 'boolean' ? value : value.toString()
2313
- return proceedSeeInField.call(this, 'assert', field, _value)
2330
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
2314
2331
  }
2315
2332
 
2316
2333
  /**
2317
2334
  * {{> dontSeeInField }}
2318
2335
  */
2319
- async dontSeeInField(field, value) {
2336
+ async dontSeeInField(field, value, context = null) {
2320
2337
  const _value = typeof value === 'boolean' ? value : value.toString()
2321
- return proceedSeeInField.call(this, 'negate', field, _value)
2338
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
2322
2339
  }
2323
2340
 
2324
2341
  /**
2325
2342
  * {{> attachFile }}
2326
2343
  *
2327
2344
  */
2328
- async attachFile(locator, pathToFile) {
2345
+ async attachFile(locator, pathToFile, context = null) {
2329
2346
  const file = path.join(global.codecept_dir, pathToFile)
2330
2347
 
2331
2348
  if (!fileExists(file)) {
2332
2349
  throw new Error(`File at ${file} can not be found on local system`)
2333
2350
  }
2334
- const els = await findFields.call(this, locator)
2335
- assertElementExists(els, locator, 'Field')
2336
- await els[0].setInputFiles(file)
2351
+ const els = await findFields.call(this, locator, context)
2352
+ if (els.length) {
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)
2356
+ if (tag === 'INPUT' && type === 'file') {
2357
+ await el.setInputFiles(file)
2358
+ return this._waitForAction()
2359
+ }
2360
+ }
2361
+
2362
+ const targetEls = els.length ? els : await this._locate(locator)
2363
+ assertElementExists(targetEls, locator, 'Element')
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)
2337
2371
  return this._waitForAction()
2338
2372
  }
2339
2373
 
2340
2374
  /**
2341
2375
  * {{> selectOption }}
2342
2376
  */
2343
- async selectOption(select, option) {
2344
- const context = await this.context
2377
+ async selectOption(select, option, context = null) {
2378
+ const pageContext = await this.context
2345
2379
  const matchedLocator = new Locator(select)
2346
2380
 
2381
+ let contextEl
2382
+ if (context) {
2383
+ const contextEls = await this._locate(context)
2384
+ assertElementExists(contextEls, context, 'Context element')
2385
+ contextEl = contextEls[0]
2386
+ }
2387
+
2347
2388
  // Strict locator
2348
2389
  if (!matchedLocator.isFuzzy()) {
2349
2390
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2350
- const els = await this._locate(matchedLocator)
2391
+ const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2351
2392
  assertElementExists(els, select, 'Selectable element')
2352
- return proceedSelect.call(this, context, els[0], option)
2393
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2353
2394
  }
2354
2395
 
2355
2396
  // Fuzzy: try combobox
2356
2397
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2357
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
2358
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2398
+ const comboboxSearchCtx = contextEl || pageContext
2399
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2400
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2359
2401
 
2360
2402
  // Fuzzy: try listbox
2361
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
2362
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2403
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2404
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2363
2405
 
2364
2406
  // Fuzzy: try native select
2365
- els = await findFields.call(this, select)
2407
+ els = await findFields.call(this, select, context)
2366
2408
  assertElementExists(els, select, 'Selectable element')
2367
- return proceedSelect.call(this, context, els[0], option)
2409
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2368
2410
  }
2369
2411
 
2370
2412
  /**
@@ -2405,6 +2447,26 @@ class Playwright extends Helper {
2405
2447
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
2406
2448
  }
2407
2449
 
2450
+ /**
2451
+ * {{> seeCurrentPathEquals }}
2452
+ */
2453
+ async seeCurrentPathEquals(path) {
2454
+ const currentUrl = await this._getPageUrl()
2455
+ const baseUrl = this.options.url || 'http://localhost'
2456
+ const actualPath = new URL(currentUrl, baseUrl).pathname
2457
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
2458
+ }
2459
+
2460
+ /**
2461
+ * {{> dontSeeCurrentPathEquals }}
2462
+ */
2463
+ async dontSeeCurrentPathEquals(path) {
2464
+ const currentUrl = await this._getPageUrl()
2465
+ const baseUrl = this.options.url || 'http://localhost'
2466
+ const actualPath = new URL(currentUrl, baseUrl).pathname
2467
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
2468
+ }
2469
+
2408
2470
  /**
2409
2471
  * {{> see }}
2410
2472
  *
@@ -2638,15 +2700,12 @@ class Playwright extends Helper {
2638
2700
  *
2639
2701
  */
2640
2702
  async grabTextFrom(locator) {
2641
- // Handle role locators with text/exact options
2642
- if (isRoleLocatorObject(locator)) {
2643
- const elements = await handleRoleLocator(this.page, locator)
2644
- if (elements && elements.length > 0) {
2645
- const text = await elements[0].textContent()
2646
- assertElementExists(text, JSON.stringify(locator))
2647
- this.debugSection('Text', text)
2648
- return text
2649
- }
2703
+ const roleElements = await handleRoleLocator(this.page, locator)
2704
+ if (roleElements && roleElements.length > 0) {
2705
+ const text = await roleElements[0].textContent()
2706
+ assertElementExists(text, JSON.stringify(locator))
2707
+ this.debugSection('Text', text)
2708
+ return text
2650
2709
  }
2651
2710
 
2652
2711
  const locatorObj = new Locator(locator, 'css')
@@ -3362,6 +3421,7 @@ class Playwright extends Helper {
3362
3421
  */
3363
3422
  async waitInUrl(urlPart, sec = null) {
3364
3423
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3424
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3365
3425
 
3366
3426
  return this.page
3367
3427
  .waitForFunction(
@@ -3369,13 +3429,13 @@ class Playwright extends Helper {
3369
3429
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
3370
3430
  return currUrl.indexOf(urlPart) > -1
3371
3431
  },
3372
- urlPart,
3432
+ expectedUrl,
3373
3433
  { timeout: waitTimeout },
3374
3434
  )
3375
3435
  .catch(async e => {
3376
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
3436
+ const currUrl = await this._getPageUrl()
3377
3437
  if (/Timeout/i.test(e.message)) {
3378
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
3438
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
3379
3439
  } else {
3380
3440
  throw e
3381
3441
  }
@@ -3387,26 +3447,46 @@ class Playwright extends Helper {
3387
3447
  */
3388
3448
  async waitUrlEquals(urlPart, sec = null) {
3389
3449
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3390
-
3391
- const baseUrl = this.options.url
3392
- let expectedUrl = urlPart
3393
- if (urlPart.indexOf('http') < 0) {
3394
- expectedUrl = baseUrl + urlPart
3395
- }
3450
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3396
3451
 
3397
3452
  try {
3398
3453
  await this.page.waitForURL(
3399
- url => url.href.includes(expectedUrl),
3454
+ url => url.href === expectedUrl,
3400
3455
  { timeout: waitTimeout },
3401
3456
  )
3402
3457
  } catch (e) {
3403
3458
  const currUrl = await this._getPageUrl()
3404
3459
  if (/Timeout/i.test(e.message)) {
3405
- if (!currUrl.includes(expectedUrl)) {
3406
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3407
- } else {
3408
- throw new Error(`expected url not loaded, error message: ${e.message}`)
3409
- }
3460
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3461
+ } else {
3462
+ throw e
3463
+ }
3464
+ }
3465
+ }
3466
+
3467
+ /**
3468
+ * {{> waitCurrentPathEquals }}
3469
+ */
3470
+ async waitCurrentPathEquals(path, sec = null) {
3471
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3472
+ const normalizedPath = normalizePath(path)
3473
+
3474
+ try {
3475
+ await this.page.waitForFunction(
3476
+ expectedPath => {
3477
+ const actualPath = window.location.pathname
3478
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
3479
+ return normalizePath(actualPath) === expectedPath
3480
+ },
3481
+ normalizedPath,
3482
+ { timeout: waitTimeout },
3483
+ )
3484
+ } catch (e) {
3485
+ const currentUrl = await this._getPageUrl()
3486
+ const baseUrl = this.options.url || 'http://localhost'
3487
+ const actualPath = new URL(currentUrl, baseUrl).pathname
3488
+ if (/Timeout/i.test(e.message)) {
3489
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
3410
3490
  } else {
3411
3491
  throw e
3412
3492
  }
@@ -4092,9 +4172,15 @@ class Playwright extends Helper {
4092
4172
 
4093
4173
  export default Playwright
4094
4174
 
4095
- function buildLocatorString(locator) {
4175
+ export function buildLocatorString(locator) {
4096
4176
  if (locator.isXPath()) {
4097
- return `xpath=${locator.value}`
4177
+ // Make XPath relative so it works correctly within scoped contexts (e.g. within()).
4178
+ // Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
4179
+ // but only when the selector starts with "/". Locator methods like at() wrap XPath in
4180
+ // parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
4181
+ // We fix this by prepending "." before the first "//" that follows any leading parentheses.
4182
+ const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
4183
+ return `xpath=${value}`
4098
4184
  }
4099
4185
  if (locator.isShadow()) {
4100
4186
  // Convert shadow locator to CSS with >> chaining operator
@@ -4105,25 +4191,22 @@ function buildLocatorString(locator) {
4105
4191
  return locator.simplify()
4106
4192
  }
4107
4193
 
4108
- /**
4109
- * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4110
- */
4111
- function isRoleLocatorObject(locator) {
4112
- return locator && typeof locator === 'object' && locator.role && !locator.type
4113
- }
4114
-
4115
4194
  /**
4116
4195
  * Handles role locator objects by converting them to Playwright's getByRole() API
4196
+ * Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
4117
4197
  * Returns elements array if role locator, null otherwise
4118
4198
  */
4119
4199
  async function handleRoleLocator(context, locator) {
4120
- if (!isRoleLocatorObject(locator)) return null
4200
+ const loc = new Locator(locator)
4201
+ if (!loc.isRole()) return null
4121
4202
 
4203
+ const roleObj = loc.locator || {}
4122
4204
  const options = {}
4123
- if (locator.text) options.name = locator.text
4124
- if (locator.exact !== undefined) options.exact = locator.exact
4205
+ if (roleObj.text) options.name = roleObj.text
4206
+ if (roleObj.name) options.name = roleObj.name
4207
+ if (roleObj.exact !== undefined) options.exact = roleObj.exact
4125
4208
 
4126
- return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4209
+ return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
4127
4210
  }
4128
4211
 
4129
4212
  async function findByRole(context, locator) {
@@ -4192,16 +4275,22 @@ async function proceedClick(locator, context = null, options = {}) {
4192
4275
  assertElementExists(els, locator, 'Clickable element')
4193
4276
  }
4194
4277
 
4195
- await highlightActiveElement.call(this, els[0])
4196
- if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
4278
+ const opts = store.currentStep?.opts
4279
+ let element
4280
+ if (opts?.elementIndex != null) {
4281
+ element = selectElement(els, locator, this)
4282
+ } else {
4283
+ const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
4284
+ if (strict) assertOnlyOneElement(els, locator, this)
4285
+ element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4286
+ }
4287
+
4288
+ await highlightActiveElement.call(this, element)
4289
+ if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
4197
4290
 
4198
- /*
4199
- using the force true options itself but instead dispatching a click
4200
- */
4201
4291
  if (options.force) {
4202
- await els[0].dispatchEvent('click')
4292
+ await element.dispatchEvent('click')
4203
4293
  } else {
4204
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4205
4294
  await element.click(options)
4206
4295
  }
4207
4296
  const promises = []
@@ -4218,7 +4307,6 @@ async function findClickable(matcher, locator) {
4218
4307
 
4219
4308
  if (!matchedLocator.isFuzzy()) {
4220
4309
  const els = await findElements.call(this, matcher, matchedLocator)
4221
- if (this.options.strict) assertOnlyOneElement(els, locator)
4222
4310
  return els
4223
4311
  }
4224
4312
 
@@ -4227,42 +4315,27 @@ async function findClickable(matcher, locator) {
4227
4315
 
4228
4316
  try {
4229
4317
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4230
- if (els.length) {
4231
- if (this.options.strict) assertOnlyOneElement(els, locator)
4232
- return els
4233
- }
4318
+ if (els.length) return els
4234
4319
  } catch (err) {
4235
4320
  // getByRole not supported or failed
4236
4321
  }
4237
4322
 
4238
4323
  try {
4239
4324
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4240
- if (els.length) {
4241
- if (this.options.strict) assertOnlyOneElement(els, locator)
4242
- return els
4243
- }
4325
+ if (els.length) return els
4244
4326
  } catch (err) {
4245
4327
  // getByRole not supported or failed
4246
4328
  }
4247
4329
 
4248
4330
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4249
- if (els.length) {
4250
- if (this.options.strict) assertOnlyOneElement(els, locator)
4251
- return els
4252
- }
4331
+ if (els.length) return els
4253
4332
 
4254
4333
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4255
- if (els.length) {
4256
- if (this.options.strict) assertOnlyOneElement(els, locator)
4257
- return els
4258
- }
4334
+ if (els.length) return els
4259
4335
 
4260
4336
  try {
4261
4337
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4262
- if (els.length) {
4263
- if (this.options.strict) assertOnlyOneElement(els, locator)
4264
- return els
4265
- }
4338
+ if (els.length) return els
4266
4339
  } catch (err) {
4267
4340
  // Do nothing
4268
4341
  }
@@ -4335,34 +4408,42 @@ async function proceedIsChecked(assertType, option) {
4335
4408
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
4336
4409
  }
4337
4410
 
4338
- async function findFields(locator) {
4339
- // Handle role locators with text/exact options
4340
- if (isRoleLocatorObject(locator)) {
4341
- const page = await this.page
4342
- const roleElements = await handleRoleLocator(page, locator)
4343
- if (roleElements) return roleElements
4411
+ async function findFields(locator, context = null) {
4412
+ let contextEl
4413
+ if (context) {
4414
+ const contextEls = await this._locate(context)
4415
+ assertElementExists(contextEls, context, 'Context element')
4416
+ contextEl = contextEls[0]
4344
4417
  }
4345
4418
 
4419
+ const locateFn = contextEl
4420
+ ? loc => findElements.call(this, contextEl, loc)
4421
+ : loc => this._locate(loc)
4422
+
4423
+ const matcher = contextEl || (await this.page)
4424
+ const roleElements = await handleRoleLocator(matcher, locator)
4425
+ if (roleElements) return roleElements
4426
+
4346
4427
  const matchedLocator = new Locator(locator)
4347
4428
  if (!matchedLocator.isFuzzy()) {
4348
- return this._locate(matchedLocator)
4429
+ return locateFn(matchedLocator)
4349
4430
  }
4350
4431
  const literal = xpathLocator.literal(locator)
4351
4432
 
4352
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
4433
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
4353
4434
  if (els.length) {
4354
4435
  return els
4355
4436
  }
4356
4437
 
4357
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
4438
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
4358
4439
  if (els.length) {
4359
4440
  return els
4360
4441
  }
4361
- els = await this._locate({ xpath: Locator.field.byName(literal) })
4442
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
4362
4443
  if (els.length) {
4363
4444
  return els
4364
4445
  }
4365
- return this._locate({ css: locator })
4446
+ return locateFn({ css: locator })
4366
4447
  }
4367
4448
 
4368
4449
  async function proceedSelect(context, el, option) {
@@ -4411,8 +4492,8 @@ async function proceedSelect(context, el, option) {
4411
4492
  return this._waitForAction()
4412
4493
  }
4413
4494
 
4414
- async function proceedSeeInField(assertType, field, value) {
4415
- const els = await findFields.call(this, field)
4495
+ async function proceedSeeInField(assertType, field, value, context) {
4496
+ const els = await findFields.call(this, field, context)
4416
4497
  assertElementExists(els, field, 'Field')
4417
4498
  const el = els[0]
4418
4499
  const tag = await el.evaluate(e => e.tagName)
@@ -4526,9 +4607,10 @@ function assertElementExists(res, locator, prefix, suffix) {
4526
4607
  }
4527
4608
  }
4528
4609
 
4529
- function assertOnlyOneElement(elements, locator) {
4610
+ function assertOnlyOneElement(elements, locator, helper) {
4530
4611
  if (elements.length > 1) {
4531
- throw new MultipleElementsFound(locator, elements)
4612
+ const webElements = elements.map(el => new WebElement(el, helper))
4613
+ throw new MultipleElementsFound(locator, webElements)
4532
4614
  }
4533
4615
  }
4534
4616