codeceptjs 4.0.1-beta.25 → 4.0.1-beta.26

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.
@@ -27,11 +27,10 @@ import {
27
27
  } from '../utils.js'
28
28
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
29
29
  import ElementNotFound from './errors/ElementNotFound.js'
30
- import MultipleElementsFound from './errors/MultipleElementsFound.js'
31
30
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
32
31
  import Popup from './extras/Popup.js'
33
32
  import Console from './extras/Console.js'
34
- import { findReact, findVue } from './extras/PlaywrightLocator.js'
33
+ import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
35
34
  import WebElement from '../element/WebElement.js'
36
35
 
37
36
  let playwright
@@ -45,6 +44,36 @@ if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
45
44
  global.__playwrightSelectorsRegistered = false
46
45
  }
47
46
 
47
+ /**
48
+ * Creates a Playwright selector engine factory for a custom locator strategy.
49
+ * @param {string} name - Strategy name for error messages
50
+ * @param {Function} func - The locator function (selector, root) => Element|Element[]
51
+ * @returns {Function} Selector engine factory
52
+ */
53
+ function createCustomSelectorEngine(name, func) {
54
+ return () => ({
55
+ create: () => null,
56
+ query(root, selector) {
57
+ if (!root) return null
58
+ try {
59
+ const result = func(selector, root)
60
+ return Array.isArray(result) ? result[0] : result
61
+ } catch (e) {
62
+ return null
63
+ }
64
+ },
65
+ queryAll(root, selector) {
66
+ if (!root) return []
67
+ try {
68
+ const result = func(selector, root)
69
+ return Array.isArray(result) ? result : result ? [result] : []
70
+ } catch (e) {
71
+ return []
72
+ }
73
+ },
74
+ })
75
+ }
76
+
48
77
  const popupStore = new Popup()
49
78
  const consoleLogStore = new Console()
50
79
  const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
@@ -108,9 +137,6 @@ const pathSeparator = path.sep
108
137
  * those cookies are used instead and the configured `storageState` is ignored (no merge).
109
138
  * May include session cookies, auth tokens, localStorage and (if captured with
110
139
  * `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
111
- * @prop {boolean} [strict=false] - throw error when multiple elements match a single-element locator.
112
- * When enabled, methods like `click`, `fillField`, `selectOption`, etc. will throw a
113
- * `MultipleElementsFound` error if more than one element matches the locator.
114
140
  */
115
141
  const config = {}
116
142
 
@@ -362,23 +388,13 @@ class Playwright extends Helper {
362
388
 
363
389
  // Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
364
390
  // This can happen in worker threads where config is serialized/deserialized
365
- let validCustomLocators = null
366
- if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
367
- // Check if it's an empty array or object with no function properties
368
- const entries = Object.entries(config.customLocatorStrategies)
369
- const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
370
- if (hasFunctions) {
371
- validCustomLocators = config.customLocatorStrategies
372
- }
373
- }
374
-
375
- this.customLocatorStrategies = validCustomLocators
391
+ this.customLocatorStrategies = this._parseCustomLocatorStrategies(config.customLocatorStrategies)
376
392
  this._customLocatorsRegistered = false
377
393
 
378
394
  // Add custom locator strategies to global registry for early registration
379
395
  if (this.customLocatorStrategies) {
380
- for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
381
- globalCustomLocatorStrategies.set(strategyName, strategyFunction)
396
+ for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
397
+ globalCustomLocatorStrategies.set(name, func)
382
398
  }
383
399
  }
384
400
 
@@ -421,7 +437,6 @@ class Playwright extends Helper {
421
437
  highlightElement: false,
422
438
  storageState: undefined,
423
439
  onResponse: null,
424
- strict: false, // Throw error when multiple elements match single-element locator
425
440
  }
426
441
 
427
442
  process.env.testIdAttribute = 'data-testid'
@@ -570,54 +585,23 @@ class Playwright extends Helper {
570
585
  }
571
586
 
572
587
  // Register all custom locator strategies from the global registry
573
- for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
574
- if (!registeredCustomLocatorStrategies.has(strategyName)) {
575
- try {
576
- // Create a selector engine factory function exactly like createValueEngine pattern
577
- // Capture variables in closure to avoid reference issues
578
- const createCustomEngine = ((name, func) => {
579
- return () => {
580
- return {
581
- create() {
582
- return null
583
- },
584
- query(root, selector) {
585
- try {
586
- if (!root) return null
587
- const result = func(selector, root)
588
- return Array.isArray(result) ? result[0] : result
589
- } catch (error) {
590
- console.warn(`Error in custom locator "${name}":`, error)
591
- return null
592
- }
593
- },
594
- queryAll(root, selector) {
595
- try {
596
- if (!root) return []
597
- const result = func(selector, root)
598
- return Array.isArray(result) ? result : result ? [result] : []
599
- } catch (error) {
600
- console.warn(`Error in custom locator "${name}":`, error)
601
- return []
602
- }
603
- },
604
- }
605
- }
606
- })(strategyName, strategyFunction)
588
+ await this._registerGlobalCustomLocators()
589
+ } catch (e) {
590
+ console.warn(e)
591
+ }
592
+ }
607
593
 
608
- await playwright.selectors.register(strategyName, createCustomEngine)
609
- registeredCustomLocatorStrategies.add(strategyName)
610
- } catch (error) {
611
- if (!error.message.includes('already registered')) {
612
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
613
- } else {
614
- console.log(`Custom locator strategy '${strategyName}' already registered`)
615
- }
616
- }
594
+ async _registerGlobalCustomLocators() {
595
+ for (const [name, func] of globalCustomLocatorStrategies.entries()) {
596
+ if (registeredCustomLocatorStrategies.has(name)) continue
597
+ try {
598
+ await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
599
+ registeredCustomLocatorStrategies.add(name)
600
+ } catch (e) {
601
+ if (!e.message.includes('already registered')) {
602
+ this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
617
603
  }
618
604
  }
619
- } catch (e) {
620
- console.warn(e)
621
605
  }
622
606
  }
623
607
 
@@ -1282,28 +1266,31 @@ class Playwright extends Helper {
1282
1266
  return this.browser
1283
1267
  }
1284
1268
 
1269
+ _hasCustomLocatorStrategies() {
1270
+ return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
1271
+ }
1272
+
1273
+ _parseCustomLocatorStrategies(strategies) {
1274
+ if (typeof strategies !== 'object' || strategies === null) return null
1275
+ const hasValidFunctions = Object.values(strategies).some(v => typeof v === 'function')
1276
+ return hasValidFunctions ? strategies : null
1277
+ }
1278
+
1285
1279
  _lookupCustomLocator(customStrategy) {
1286
- if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
1287
- return null
1288
- }
1280
+ if (!this._hasCustomLocatorStrategies()) return null
1289
1281
  const strategy = this.customLocatorStrategies[customStrategy]
1290
1282
  return typeof strategy === 'function' ? strategy : null
1291
1283
  }
1292
1284
 
1293
1285
  _isCustomLocator(locator) {
1294
1286
  const locatorObj = new Locator(locator)
1295
- if (locatorObj.isCustom()) {
1296
- const customLocator = this._lookupCustomLocator(locatorObj.type)
1297
- if (customLocator) {
1298
- return true
1299
- }
1300
- throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
1301
- }
1302
- return false
1287
+ if (!locatorObj.isCustom()) return false
1288
+ if (this._lookupCustomLocator(locatorObj.type)) return true
1289
+ throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
1303
1290
  }
1304
1291
 
1305
1292
  _isCustomLocatorStrategyDefined() {
1306
- return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
1293
+ return this._hasCustomLocatorStrategies()
1307
1294
  }
1308
1295
 
1309
1296
  /**
@@ -1326,49 +1313,16 @@ class Playwright extends Helper {
1326
1313
  }
1327
1314
 
1328
1315
  async _registerCustomLocatorStrategies() {
1329
- if (!this.customLocatorStrategies) return
1330
-
1331
- for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
1332
- if (!registeredCustomLocatorStrategies.has(strategyName)) {
1333
- try {
1334
- const createCustomEngine = ((name, func) => {
1335
- return () => {
1336
- return {
1337
- create(root, target) {
1338
- return null
1339
- },
1340
- query(root, selector) {
1341
- try {
1342
- if (!root) return null
1343
- const result = func(selector, root)
1344
- return Array.isArray(result) ? result[0] : result
1345
- } catch (error) {
1346
- console.warn(`Error in custom locator "${name}":`, error)
1347
- return null
1348
- }
1349
- },
1350
- queryAll(root, selector) {
1351
- try {
1352
- if (!root) return []
1353
- const result = func(selector, root)
1354
- return Array.isArray(result) ? result : result ? [result] : []
1355
- } catch (error) {
1356
- console.warn(`Error in custom locator "${name}":`, error)
1357
- return []
1358
- }
1359
- },
1360
- }
1361
- }
1362
- })(strategyName, strategyFunction)
1316
+ if (!this._hasCustomLocatorStrategies()) return
1363
1317
 
1364
- await playwright.selectors.register(strategyName, createCustomEngine)
1365
- registeredCustomLocatorStrategies.add(strategyName)
1366
- } catch (error) {
1367
- if (!error.message.includes('already registered')) {
1368
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
1369
- } else {
1370
- console.log(`Custom locator strategy '${strategyName}' already registered`)
1371
- }
1318
+ for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
1319
+ if (registeredCustomLocatorStrategies.has(name)) continue
1320
+ try {
1321
+ await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
1322
+ registeredCustomLocatorStrategies.add(name)
1323
+ } catch (e) {
1324
+ if (!e.message.includes('already registered')) {
1325
+ this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
1372
1326
  }
1373
1327
  }
1374
1328
  }
@@ -1917,12 +1871,7 @@ class Playwright extends Helper {
1917
1871
  */
1918
1872
  async _locateElement(locator) {
1919
1873
  const context = await this._getContext()
1920
- const elements = await findElements.call(this, context, locator)
1921
- if (elements.length === 0) {
1922
- throw new ElementNotFound(locator, 'Element', 'was not found')
1923
- }
1924
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1925
- return elements[0]
1874
+ return findElement(context, locator)
1926
1875
  }
1927
1876
 
1928
1877
  /**
@@ -1937,7 +1886,6 @@ class Playwright extends Helper {
1937
1886
  const context = providedContext || (await this._getContext())
1938
1887
  const els = await findCheckable.call(this, locator, context)
1939
1888
  assertElementExists(els[0], locator, 'Checkbox or radio')
1940
- if (this.options.strict) assertOnlyOneElement(els, locator)
1941
1889
  return els[0]
1942
1890
  }
1943
1891
 
@@ -2410,7 +2358,6 @@ class Playwright extends Helper {
2410
2358
  async fillField(field, value) {
2411
2359
  const els = await findFields.call(this, field)
2412
2360
  assertElementExists(els, field, 'Field')
2413
- if (this.options.strict) assertOnlyOneElement(els, field)
2414
2361
  const el = els[0]
2415
2362
 
2416
2363
  await el.clear()
@@ -2443,7 +2390,6 @@ class Playwright extends Helper {
2443
2390
  async clearField(locator, options = {}) {
2444
2391
  const els = await findFields.call(this, locator)
2445
2392
  assertElementExists(els, locator, 'Field to clear')
2446
- if (this.options.strict) assertOnlyOneElement(els, locator)
2447
2393
 
2448
2394
  const el = els[0]
2449
2395
 
@@ -2460,7 +2406,6 @@ class Playwright extends Helper {
2460
2406
  async appendField(field, value) {
2461
2407
  const els = await findFields.call(this, field)
2462
2408
  assertElementExists(els, field, 'Field')
2463
- if (this.options.strict) assertOnlyOneElement(els, field)
2464
2409
  await highlightActiveElement.call(this, els[0])
2465
2410
  await els[0].press('End')
2466
2411
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
@@ -2503,28 +2448,23 @@ class Playwright extends Helper {
2503
2448
  * {{> selectOption }}
2504
2449
  */
2505
2450
  async selectOption(select, option) {
2506
- const context = await this.context
2507
- const matchedLocator = new Locator(select)
2508
-
2509
- if (!matchedLocator.isFuzzy()) {
2510
- this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2511
- const els = await this._locate(matchedLocator)
2512
- assertElementExists(els, select, 'Selectable element')
2513
- return proceedSelect.call(this, context, els[0], option)
2514
- }
2451
+ const els = await findFields.call(this, select)
2452
+ assertElementExists(els, select, 'Selectable field')
2453
+ const el = els[0]
2515
2454
 
2516
- this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2517
- const literal = xpathLocator.literal(matchedLocator.value)
2455
+ await highlightActiveElement.call(this, el)
2456
+ let optionToSelect = ''
2518
2457
 
2519
- let els = await this._locate({ xpath: Locator.select.narrow(literal) })
2520
- if (els.length) return proceedSelect.call(this, context, els[0], option)
2458
+ try {
2459
+ optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim()
2460
+ } catch (e) {
2461
+ optionToSelect = option
2462
+ }
2521
2463
 
2522
- els = await this._locate({ xpath: Locator.select.wide(literal) })
2523
- if (els.length) return proceedSelect.call(this, context, els[0], option)
2464
+ if (!Array.isArray(option)) option = [optionToSelect]
2524
2465
 
2525
- els = await findFields.call(this, select)
2526
- assertElementExists(els, select, 'Selectable element')
2527
- return proceedSelect.call(this, context, els[0], option)
2466
+ await el.selectOption(option)
2467
+ return this._waitForAction()
2528
2468
  }
2529
2469
 
2530
2470
  /**
@@ -2571,10 +2511,6 @@ class Playwright extends Helper {
2571
2511
  *
2572
2512
  */
2573
2513
  async see(text, context = null) {
2574
- // If only one argument passed and it's an object without custom toString(), treat as locator
2575
- if (!context && text && typeof text === 'object' && !Array.isArray(text) && text.toString === Object.prototype.toString) {
2576
- return this.seeElement(text)
2577
- }
2578
2514
  return proceedSee.call(this, 'assert', text, context)
2579
2515
  }
2580
2516
 
@@ -2813,6 +2749,17 @@ class Playwright extends Helper {
2813
2749
  *
2814
2750
  */
2815
2751
  async grabTextFrom(locator) {
2752
+ // Handle role locators with text/exact options
2753
+ if (isRoleLocatorObject(locator)) {
2754
+ const elements = await handleRoleLocator(this.page, locator)
2755
+ if (elements && elements.length > 0) {
2756
+ const text = await elements[0].textContent()
2757
+ assertElementExists(text, JSON.stringify(locator))
2758
+ this.debugSection('Text', text)
2759
+ return text
2760
+ }
2761
+ }
2762
+
2816
2763
  const locatorObj = new Locator(locator, 'css')
2817
2764
 
2818
2765
  if (locatorObj.isCustom()) {
@@ -2825,32 +2772,21 @@ class Playwright extends Helper {
2825
2772
  assertElementExists(text, locatorObj.toString())
2826
2773
  this.debugSection('Text', text)
2827
2774
  return text
2828
- }
2829
-
2830
- if (locatorObj.isRole()) {
2831
- // Handle role locators with text/exact options
2832
- const roleElements = await findByRole(this.page, locator)
2833
- if (roleElements && roleElements.length > 0) {
2834
- const text = await roleElements[0].textContent()
2835
- assertElementExists(text, JSON.stringify(locator))
2775
+ } else {
2776
+ locator = this._contextLocator(locator)
2777
+ try {
2778
+ const text = await this.page.textContent(locator)
2779
+ assertElementExists(text, locator)
2836
2780
  this.debugSection('Text', text)
2837
2781
  return text
2782
+ } catch (error) {
2783
+ // Convert Playwright timeout errors to ElementNotFound for consistency
2784
+ if (error.message && error.message.includes('Timeout')) {
2785
+ throw new ElementNotFound(locator, 'text')
2786
+ }
2787
+ throw error
2838
2788
  }
2839
2789
  }
2840
-
2841
- locator = this._contextLocator(locator)
2842
- try {
2843
- const text = await this.page.textContent(locator)
2844
- assertElementExists(text, locator)
2845
- this.debugSection('Text', text)
2846
- return text
2847
- } catch (error) {
2848
- // Convert Playwright timeout errors to ElementNotFound for consistency
2849
- if (error.message && error.message.includes('Timeout')) {
2850
- throw new ElementNotFound(locator, 'text')
2851
- }
2852
- throw error
2853
- }
2854
2790
  }
2855
2791
 
2856
2792
  /**
@@ -4329,26 +4265,50 @@ function buildLocatorString(locator) {
4329
4265
  if (locator.isXPath()) {
4330
4266
  return `xpath=${locator.value}`
4331
4267
  }
4332
- return locator.simplify()
4268
+ return locator.simplify()
4269
+ }
4270
+
4271
+ /**
4272
+ * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4273
+ */
4274
+ function isRoleLocatorObject(locator) {
4275
+ return locator && typeof locator === 'object' && locator.role && !locator.type
4333
4276
  }
4334
4277
 
4335
- async function findByRole(context, locator) {
4336
- const matchedLocator = Locator.from(locator)
4337
- if (!matchedLocator.isRole()) return null
4338
- const roleOptions = matchedLocator.getRoleOptions()
4339
- return context.getByRole(roleOptions.role, roleOptions.options).all()
4278
+ /**
4279
+ * Handles role locator objects by converting them to Playwright's getByRole() API
4280
+ * Returns elements array if role locator, null otherwise
4281
+ */
4282
+ async function handleRoleLocator(context, locator) {
4283
+ if (!isRoleLocatorObject(locator)) return null
4284
+
4285
+ const options = {}
4286
+ if (locator.text) options.name = locator.text
4287
+ if (locator.exact !== undefined) options.exact = locator.exact
4288
+
4289
+ return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4340
4290
  }
4341
4291
 
4342
4292
  async function findElements(matcher, locator) {
4343
- const matchedLocator = Locator.from(locator)
4344
- const roleElements = await findByRole(matcher, matchedLocator)
4293
+ // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4294
+ const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4295
+ const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4296
+ const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4297
+
4298
+ if (isReactLocator) return findReact(matcher, locator)
4299
+ if (isVueLocator) return findVue(matcher, locator)
4300
+ if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4301
+
4302
+ // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
4303
+ const roleElements = await handleRoleLocator(matcher, locator)
4345
4304
  if (roleElements) return roleElements
4346
4305
 
4347
- const isReactLocator = matchedLocator.type === 'react'
4348
- const isVueLocator = matchedLocator.type === 'vue'
4306
+ locator = new Locator(locator, 'css')
4349
4307
 
4350
- if (isReactLocator) return findReact(matcher, matchedLocator)
4351
- if (isVueLocator) return findVue(matcher, matchedLocator)
4308
+ // Handle custom locators directly instead of relying on Playwright selector engines
4309
+ if (locator.isCustom()) {
4310
+ return findCustomElements.call(this, matcher, locator)
4311
+ }
4352
4312
 
4353
4313
  // Check if we have a custom context locator and need to search within it
4354
4314
  if (this.contextLocator) {
@@ -4361,12 +4321,12 @@ async function findElements(matcher, locator) {
4361
4321
  }
4362
4322
 
4363
4323
  // Search within the first context element
4364
- const locatorString = buildLocatorString(matchedLocator)
4324
+ const locatorString = buildLocatorString(locator)
4365
4325
  return contextElements[0].locator(locatorString).all()
4366
4326
  }
4367
4327
  }
4368
4328
 
4369
- const locatorString = buildLocatorString(matchedLocator)
4329
+ const locatorString = buildLocatorString(locator)
4370
4330
 
4371
4331
  return matcher.locator(locatorString).all()
4372
4332
  }
@@ -4458,6 +4418,16 @@ async function findCustomElements(matcher, locator) {
4458
4418
  return locators
4459
4419
  }
4460
4420
 
4421
+ async function findElement(matcher, locator) {
4422
+ if (locator.react) return findReact(matcher, locator)
4423
+ if (locator.vue) return findVue(matcher, locator)
4424
+ if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4425
+
4426
+ locator = new Locator(locator, 'css')
4427
+
4428
+ return matcher.locator(buildLocatorString(locator)).first()
4429
+ }
4430
+
4461
4431
  async function getVisibleElements(elements) {
4462
4432
  const visibleElements = []
4463
4433
  for (const element of elements) {
@@ -4507,42 +4477,41 @@ async function proceedClick(locator, context = null, options = {}) {
4507
4477
  }
4508
4478
 
4509
4479
  async function findClickable(matcher, locator) {
4510
- if (locator.react) return findReact(matcher, locator)
4511
- if (locator.vue) return findVue(matcher, locator)
4480
+ const matchedLocator = new Locator(locator)
4512
4481
 
4513
- locator = new Locator(locator)
4514
- if (!locator.isFuzzy()) {
4515
- const els = await findElements.call(this, matcher, locator)
4516
- if (this.options.strict) assertOnlyOneElement(els, locator)
4517
- return els
4518
- }
4482
+ if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
4519
4483
 
4520
4484
  let els
4521
- const literal = xpathLocator.literal(locator.value)
4485
+ const literal = xpathLocator.literal(matchedLocator.value)
4522
4486
 
4523
- els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4524
- if (els.length) {
4525
- if (this.options.strict) assertOnlyOneElement(els, locator)
4526
- return els
4487
+ try {
4488
+ els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4489
+ if (els.length) return els
4490
+ } catch (err) {
4491
+ // getByRole not supported or failed
4527
4492
  }
4528
4493
 
4529
- els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4530
- if (els.length) {
4531
- if (this.options.strict) assertOnlyOneElement(els, locator)
4532
- return els
4494
+ try {
4495
+ els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4496
+ if (els.length) return els
4497
+ } catch (err) {
4498
+ // getByRole not supported or failed
4533
4499
  }
4534
4500
 
4501
+ els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4502
+ if (els.length) return els
4503
+
4504
+ els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4505
+ if (els.length) return els
4506
+
4535
4507
  try {
4536
4508
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4537
- if (els.length) {
4538
- if (this.options.strict) assertOnlyOneElement(els, locator)
4539
- return els
4540
- }
4509
+ if (els.length) return els
4541
4510
  } catch (err) {
4542
4511
  // Do nothing
4543
4512
  }
4544
4513
 
4545
- return findElements.call(this, matcher, locator.value) // by css or xpath
4514
+ return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
4546
4515
  }
4547
4516
 
4548
4517
  async function proceedSee(assertType, text, context, strict = false) {
@@ -4582,17 +4551,14 @@ async function findCheckable(locator, context) {
4582
4551
  }
4583
4552
 
4584
4553
  // Handle role locators with text/exact options
4585
- const roleElements = await findByRole(contextEl, locator)
4554
+ const roleElements = await handleRoleLocator(contextEl, locator)
4586
4555
  if (roleElements) return roleElements
4587
4556
 
4588
- const matchedLocator = Locator.from(locator)
4557
+ const matchedLocator = new Locator(locator)
4589
4558
  if (!matchedLocator.isFuzzy()) {
4590
4559
  return findElements.call(this, contextEl, matchedLocator)
4591
4560
  }
4592
4561
 
4593
- const checkboxByRole = await findByRole(contextEl, { role: 'checkbox', name: matchedLocator.value })
4594
- if (checkboxByRole) return checkboxByRole
4595
-
4596
4562
  const literal = xpathLocator.literal(matchedLocator.value)
4597
4563
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
4598
4564
  if (els.length) {
@@ -4614,15 +4580,17 @@ async function proceedIsChecked(assertType, option) {
4614
4580
  }
4615
4581
 
4616
4582
  async function findFields(locator) {
4617
- const page = await this.page
4618
- const roleElements = await findByRole(page, locator)
4619
- if (roleElements) return roleElements
4583
+ // Handle role locators with text/exact options
4584
+ if (isRoleLocatorObject(locator)) {
4585
+ const page = await this.page
4586
+ const roleElements = await handleRoleLocator(page, locator)
4587
+ if (roleElements) return roleElements
4588
+ }
4620
4589
 
4621
4590
  const matchedLocator = new Locator(locator)
4622
4591
  if (!matchedLocator.isFuzzy()) {
4623
4592
  return this._locate(matchedLocator)
4624
4593
  }
4625
-
4626
4594
  const literal = xpathLocator.literal(locator)
4627
4595
 
4628
4596
  let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
@@ -4641,45 +4609,6 @@ async function findFields(locator) {
4641
4609
  return this._locate({ css: locator })
4642
4610
  }
4643
4611
 
4644
- async function proceedSelect(context, el, option) {
4645
- const role = await el.getAttribute('role')
4646
- const options = Array.isArray(option) ? option : [option]
4647
-
4648
- if (role === 'combobox' || role === 'button') {
4649
- this.debugSection('SelectOption', 'Expanding combobox')
4650
- await highlightActiveElement.call(this, el)
4651
- const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
4652
- await el.click()
4653
- await this._waitForAction()
4654
-
4655
- const listboxId = ariaOwns || ariaControls
4656
- let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
4657
- if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
4658
-
4659
- for (const opt of options) {
4660
- const optEl = listbox.getByRole('option', { name: opt }).first()
4661
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
4662
- await highlightActiveElement.call(this, optEl)
4663
- await optEl.click()
4664
- }
4665
- return this._waitForAction()
4666
- }
4667
-
4668
- if (role === 'listbox') {
4669
- for (const opt of options) {
4670
- const optEl = el.getByRole('option', { name: opt }).first()
4671
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
4672
- await highlightActiveElement.call(this, optEl)
4673
- await optEl.click()
4674
- }
4675
- return this._waitForAction()
4676
- }
4677
-
4678
- await highlightActiveElement.call(this, el)
4679
- await el.selectOption(option)
4680
- return this._waitForAction()
4681
- }
4682
-
4683
4612
  async function proceedSeeInField(assertType, field, value) {
4684
4613
  const els = await findFields.call(this, field)
4685
4614
  assertElementExists(els, field, 'Field')
@@ -4795,12 +4724,6 @@ function assertElementExists(res, locator, prefix, suffix) {
4795
4724
  }
4796
4725
  }
4797
4726
 
4798
- function assertOnlyOneElement(elements, locator) {
4799
- if (elements.length > 1) {
4800
- throw new MultipleElementsFound(locator, elements)
4801
- }
4802
- }
4803
-
4804
4727
  function $XPath(element, selector) {
4805
4728
  const found = document.evaluate(selector, element || document.body, null, 5, null)
4806
4729
  const res = []
@@ -5037,7 +4960,7 @@ async function saveTraceForContext(context, name) {
5037
4960
  }
5038
4961
 
5039
4962
  async function highlightActiveElement(element) {
5040
- if (this.options.highlightElement || store.onPause || store.debugMode) {
4963
+ if ((this.options.highlightElement || store.onPause) && store.debugMode) {
5041
4964
  await element.evaluate(el => {
5042
4965
  const prevStyle = el.style.boxShadow
5043
4966
  el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes