codeceptjs 3.7.4 → 3.7.5-beta.2

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.
@@ -33,10 +33,13 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
33
33
  const Popup = require('./extras/Popup')
34
34
  const Console = require('./extras/Console')
35
35
  const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator')
36
+ const WebElement = require('../element/WebElement')
36
37
 
37
38
  let playwright
38
39
  let perfTiming
39
40
  let defaultSelectorEnginesInitialized = false
41
+ let registeredCustomLocatorStrategies = new Set()
42
+ let globalCustomLocatorStrategies = new Map()
40
43
 
41
44
  const popupStore = new Popup()
42
45
  const consoleLogStore = new Console()
@@ -95,6 +98,7 @@ const pathSeparator = path.sep
95
98
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
96
99
  * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
97
100
  * @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
101
+ * @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(\`[role="\${selector}\"]\`) } }`
98
102
  */
99
103
  const config = {}
100
104
 
@@ -343,9 +347,23 @@ class Playwright extends Helper {
343
347
  this.recordingWebSocketMessages = false
344
348
  this.recordedWebSocketMessagesAtLeastOnce = false
345
349
  this.cdpSession = null
350
+ this.customLocatorStrategies = typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null ? config.customLocatorStrategies : null
351
+ this._customLocatorsRegistered = false
352
+
353
+ // Add custom locator strategies to global registry for early registration
354
+ if (this.customLocatorStrategies) {
355
+ for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
356
+ globalCustomLocatorStrategies.set(strategyName, strategyFunction)
357
+ }
358
+ }
346
359
 
347
360
  // override defaults with config
348
361
  this._setConfig(config)
362
+
363
+ // Call _init() to register selector engines - use setTimeout to avoid blocking constructor
364
+ setTimeout(() => {
365
+ this._init().catch(console.error)
366
+ }, 0)
349
367
  }
350
368
 
351
369
  _validateConfig(config) {
@@ -462,12 +480,61 @@ class Playwright extends Helper {
462
480
 
463
481
  async _init() {
464
482
  // register an internal selector engine for reading value property of elements in a selector
465
- if (defaultSelectorEnginesInitialized) return
466
- defaultSelectorEnginesInitialized = true
467
483
  try {
468
- await playwright.selectors.register('__value', createValueEngine)
469
- await playwright.selectors.register('__disabled', createDisabledEngine)
470
- if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
484
+ if (!defaultSelectorEnginesInitialized) {
485
+ await playwright.selectors.register('__value', createValueEngine)
486
+ await playwright.selectors.register('__disabled', createDisabledEngine)
487
+ if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
488
+ defaultSelectorEnginesInitialized = true
489
+ }
490
+
491
+ // Register all custom locator strategies from the global registry
492
+ for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
493
+ if (!registeredCustomLocatorStrategies.has(strategyName)) {
494
+ try {
495
+ // Create a selector engine factory function exactly like createValueEngine pattern
496
+ // Capture variables in closure to avoid reference issues
497
+ const createCustomEngine = ((name, func) => {
498
+ return () => {
499
+ return {
500
+ create() {
501
+ return null
502
+ },
503
+ query(root, selector) {
504
+ try {
505
+ if (!root) return null
506
+ const result = func(selector, root)
507
+ return Array.isArray(result) ? result[0] : result
508
+ } catch (error) {
509
+ console.warn(`Error in custom locator "${name}":`, error)
510
+ return null
511
+ }
512
+ },
513
+ queryAll(root, selector) {
514
+ try {
515
+ if (!root) return []
516
+ const result = func(selector, root)
517
+ return Array.isArray(result) ? result : result ? [result] : []
518
+ } catch (error) {
519
+ console.warn(`Error in custom locator "${name}":`, error)
520
+ return []
521
+ }
522
+ },
523
+ }
524
+ }
525
+ })(strategyName, strategyFunction)
526
+
527
+ await playwright.selectors.register(strategyName, createCustomEngine)
528
+ registeredCustomLocatorStrategies.add(strategyName)
529
+ } catch (error) {
530
+ if (!error.message.includes('already registered')) {
531
+ console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
532
+ } else {
533
+ console.log(`Custom locator strategy '${strategyName}' already registered`)
534
+ }
535
+ }
536
+ }
537
+ }
471
538
  } catch (e) {
472
539
  console.warn(e)
473
540
  }
@@ -826,6 +893,9 @@ class Playwright extends Helper {
826
893
  }
827
894
 
828
895
  async _startBrowser() {
896
+ // Ensure custom locator strategies are registered before browser launch
897
+ await this._init()
898
+
829
899
  if (this.isElectron) {
830
900
  this.browser = await playwright._electron.launch(this.playwrightOptions)
831
901
  } else if (this.isRemoteBrowser && this.isCDPConnection) {
@@ -861,6 +931,30 @@ class Playwright extends Helper {
861
931
  return this.browser
862
932
  }
863
933
 
934
+ _lookupCustomLocator(customStrategy) {
935
+ if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
936
+ return null
937
+ }
938
+ const strategy = this.customLocatorStrategies[customStrategy]
939
+ return typeof strategy === 'function' ? strategy : null
940
+ }
941
+
942
+ _isCustomLocator(locator) {
943
+ const locatorObj = new Locator(locator)
944
+ if (locatorObj.isCustom()) {
945
+ const customLocator = this._lookupCustomLocator(locatorObj.type)
946
+ if (customLocator) {
947
+ return true
948
+ }
949
+ throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
950
+ }
951
+ return false
952
+ }
953
+
954
+ _isCustomLocatorStrategyDefined() {
955
+ return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
956
+ }
957
+
864
958
  /**
865
959
  * Create a new browser context with a page. \
866
960
  * Usually it should be run from a custom helper after call of `_startBrowser()`
@@ -868,11 +962,64 @@ class Playwright extends Helper {
868
962
  */
869
963
  async _createContextPage(contextOptions) {
870
964
  this.browserContext = await this.browser.newContext(contextOptions)
965
+
966
+ // Register custom locator strategies for this context
967
+ await this._registerCustomLocatorStrategies()
968
+
871
969
  const page = await this.browserContext.newPage()
872
970
  targetCreatedHandler.call(this, page)
873
971
  await this._setPage(page)
874
972
  }
875
973
 
974
+ async _registerCustomLocatorStrategies() {
975
+ if (!this.customLocatorStrategies) return
976
+
977
+ for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
978
+ if (!registeredCustomLocatorStrategies.has(strategyName)) {
979
+ try {
980
+ const createCustomEngine = ((name, func) => {
981
+ return () => {
982
+ return {
983
+ create(root, target) {
984
+ return null
985
+ },
986
+ query(root, selector) {
987
+ try {
988
+ if (!root) return null
989
+ const result = func(selector, root)
990
+ return Array.isArray(result) ? result[0] : result
991
+ } catch (error) {
992
+ console.warn(`Error in custom locator "${name}":`, error)
993
+ return null
994
+ }
995
+ },
996
+ queryAll(root, selector) {
997
+ try {
998
+ if (!root) return []
999
+ const result = func(selector, root)
1000
+ return Array.isArray(result) ? result : result ? [result] : []
1001
+ } catch (error) {
1002
+ console.warn(`Error in custom locator "${name}":`, error)
1003
+ return []
1004
+ }
1005
+ },
1006
+ }
1007
+ }
1008
+ })(strategyName, strategyFunction)
1009
+
1010
+ await playwright.selectors.register(strategyName, createCustomEngine)
1011
+ registeredCustomLocatorStrategies.add(strategyName)
1012
+ } catch (error) {
1013
+ if (!error.message.includes('already registered')) {
1014
+ console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
1015
+ } else {
1016
+ console.log(`Custom locator strategy '${strategyName}' already registered`)
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+
876
1023
  _getType() {
877
1024
  return this.browser._type
878
1025
  }
@@ -884,7 +1031,10 @@ class Playwright extends Helper {
884
1031
  this.frame = null
885
1032
  popupStore.clear()
886
1033
  if (this.options.recordHar) await this.browserContext.close()
1034
+ this.browserContext = null
887
1035
  await this.browser.close()
1036
+ this.browser = null
1037
+ this.isRunning = false
888
1038
  }
889
1039
 
890
1040
  async _evaluateHandeInContext(...args) {
@@ -1265,9 +1415,9 @@ class Playwright extends Helper {
1265
1415
  async _locate(locator) {
1266
1416
  const context = await this._getContext()
1267
1417
 
1268
- if (this.frame) return findElements(this.frame, locator)
1418
+ if (this.frame) return findElements.call(this, this.frame, locator)
1269
1419
 
1270
- const els = await findElements(context, locator)
1420
+ const els = await findElements.call(this, context, locator)
1271
1421
 
1272
1422
  if (store.debugMode) {
1273
1423
  const previewElements = els.slice(0, 3)
@@ -1341,7 +1491,8 @@ class Playwright extends Helper {
1341
1491
  *
1342
1492
  */
1343
1493
  async grabWebElements(locator) {
1344
- return this._locate(locator)
1494
+ const elements = await this._locate(locator)
1495
+ return elements.map(element => new WebElement(element, this))
1345
1496
  }
1346
1497
 
1347
1498
  /**
@@ -1349,7 +1500,8 @@ class Playwright extends Helper {
1349
1500
  *
1350
1501
  */
1351
1502
  async grabWebElement(locator) {
1352
- return this._locateElement(locator)
1503
+ const element = await this._locateElement(locator)
1504
+ return new WebElement(element, this)
1353
1505
  }
1354
1506
 
1355
1507
  /**
@@ -2060,11 +2212,25 @@ class Playwright extends Helper {
2060
2212
  * @param {*} locator
2061
2213
  */
2062
2214
  _contextLocator(locator) {
2063
- locator = buildLocatorString(new Locator(locator, 'css'))
2215
+ const locatorObj = new Locator(locator, 'css')
2216
+
2217
+ // Handle custom locators differently
2218
+ if (locatorObj.isCustom()) {
2219
+ return buildCustomLocatorString(locatorObj)
2220
+ }
2221
+
2222
+ locator = buildLocatorString(locatorObj)
2064
2223
 
2065
2224
  if (this.contextLocator) {
2066
- const contextLocator = buildLocatorString(new Locator(this.contextLocator, 'css'))
2067
- locator = `${contextLocator} >> ${locator}`
2225
+ const contextLocatorObj = new Locator(this.contextLocator, 'css')
2226
+ if (contextLocatorObj.isCustom()) {
2227
+ // For custom context locators, we can't use the >> syntax
2228
+ // Instead, we'll need to handle this differently in the calling methods
2229
+ return locator
2230
+ } else {
2231
+ const contextLocator = buildLocatorString(contextLocatorObj)
2232
+ locator = `${contextLocator} >> ${locator}`
2233
+ }
2068
2234
  }
2069
2235
 
2070
2236
  return locator
@@ -2075,11 +2241,25 @@ class Playwright extends Helper {
2075
2241
  *
2076
2242
  */
2077
2243
  async grabTextFrom(locator) {
2078
- locator = this._contextLocator(locator)
2079
- const text = await this.page.textContent(locator)
2080
- assertElementExists(text, locator)
2081
- this.debugSection('Text', text)
2082
- return text
2244
+ const locatorObj = new Locator(locator, 'css')
2245
+
2246
+ if (locatorObj.isCustom()) {
2247
+ // For custom locators, find the element first
2248
+ const elements = await findCustomElements.call(this, this.page, locatorObj)
2249
+ if (elements.length === 0) {
2250
+ throw new Error(`Element not found: ${locatorObj.toString()}`)
2251
+ }
2252
+ const text = await elements[0].textContent()
2253
+ assertElementExists(text, locatorObj.toString())
2254
+ this.debugSection('Text', text)
2255
+ return text
2256
+ } else {
2257
+ locator = this._contextLocator(locator)
2258
+ const text = await this.page.textContent(locator)
2259
+ assertElementExists(text, locator)
2260
+ this.debugSection('Text', text)
2261
+ return text
2262
+ }
2083
2263
  }
2084
2264
 
2085
2265
  /**
@@ -2092,7 +2272,6 @@ class Playwright extends Helper {
2092
2272
  for (const el of els) {
2093
2273
  texts.push(await el.innerText())
2094
2274
  }
2095
- this.debug(`Matched ${els.length} elements`)
2096
2275
  return texts
2097
2276
  }
2098
2277
 
@@ -2111,7 +2290,6 @@ class Playwright extends Helper {
2111
2290
  */
2112
2291
  async grabValueFromAll(locator) {
2113
2292
  const els = await findFields.call(this, locator)
2114
- this.debug(`Matched ${els.length} elements`)
2115
2293
  return Promise.all(els.map(el => el.inputValue()))
2116
2294
  }
2117
2295
 
@@ -2130,7 +2308,6 @@ class Playwright extends Helper {
2130
2308
  */
2131
2309
  async grabHTMLFromAll(locator) {
2132
2310
  const els = await this._locate(locator)
2133
- this.debug(`Matched ${els.length} elements`)
2134
2311
  return Promise.all(els.map(el => el.innerHTML()))
2135
2312
  }
2136
2313
 
@@ -2151,7 +2328,6 @@ class Playwright extends Helper {
2151
2328
  */
2152
2329
  async grabCssPropertyFromAll(locator, cssProperty) {
2153
2330
  const els = await this._locate(locator)
2154
- this.debug(`Matched ${els.length} elements`)
2155
2331
  const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2156
2332
 
2157
2333
  return cssValues
@@ -2262,7 +2438,6 @@ class Playwright extends Helper {
2262
2438
  */
2263
2439
  async grabAttributeFromAll(locator, attr) {
2264
2440
  const els = await this._locate(locator)
2265
- this.debug(`Matched ${els.length} elements`)
2266
2441
  const array = []
2267
2442
 
2268
2443
  for (let index = 0; index < els.length; index++) {
@@ -2282,7 +2457,6 @@ class Playwright extends Helper {
2282
2457
  const res = await this._locateElement(locator)
2283
2458
  assertElementExists(res, locator)
2284
2459
  const elem = res
2285
- this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
2286
2460
  return elem.screenshot({ path: outputFile, type: 'png' })
2287
2461
  }
2288
2462
 
@@ -2577,7 +2751,16 @@ class Playwright extends Helper {
2577
2751
 
2578
2752
  const context = await this._getContext()
2579
2753
  try {
2580
- await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2754
+ if (locator.isCustom()) {
2755
+ // For custom locators, we need to use our custom element finding logic
2756
+ const elements = await findCustomElements.call(this, context, locator)
2757
+ if (elements.length === 0) {
2758
+ throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
2759
+ }
2760
+ await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
2761
+ } else {
2762
+ await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2763
+ }
2581
2764
  } catch (e) {
2582
2765
  throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2583
2766
  }
@@ -2591,9 +2774,30 @@ class Playwright extends Helper {
2591
2774
  async waitForVisible(locator, sec) {
2592
2775
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2593
2776
  locator = new Locator(locator, 'css')
2777
+
2594
2778
  const context = await this._getContext()
2595
2779
  let count = 0
2596
2780
 
2781
+ // Handle custom locators
2782
+ if (locator.isCustom()) {
2783
+ let waiter
2784
+ do {
2785
+ const elements = await findCustomElements.call(this, context, locator)
2786
+ if (elements.length > 0) {
2787
+ waiter = await elements[0].isVisible()
2788
+ } else {
2789
+ waiter = false
2790
+ }
2791
+ if (!waiter) {
2792
+ await this.wait(1)
2793
+ count += 1000
2794
+ }
2795
+ } while (!waiter && count <= waitTimeout)
2796
+
2797
+ if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
2798
+ return
2799
+ }
2800
+
2597
2801
  // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
2598
2802
  let waiter
2599
2803
  if (this.frame) {
@@ -2620,6 +2824,7 @@ class Playwright extends Helper {
2620
2824
  async waitForInvisible(locator, sec) {
2621
2825
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2622
2826
  locator = new Locator(locator, 'css')
2827
+
2623
2828
  const context = await this._getContext()
2624
2829
  let waiter
2625
2830
  let count = 0
@@ -2650,6 +2855,7 @@ class Playwright extends Helper {
2650
2855
  async waitToHide(locator, sec) {
2651
2856
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2652
2857
  locator = new Locator(locator, 'css')
2858
+
2653
2859
  const context = await this._getContext()
2654
2860
  let waiter
2655
2861
  let count = 0
@@ -2771,52 +2977,77 @@ class Playwright extends Helper {
2771
2977
  if (context) {
2772
2978
  const locator = new Locator(context, 'css')
2773
2979
  try {
2980
+ if (locator.isCustom()) {
2981
+ // For custom locators, find the elements first then check for text within them
2982
+ const elements = await findCustomElements.call(this, contextObject, locator)
2983
+ if (elements.length === 0) {
2984
+ throw new Error(`Context element not found: ${locator.toString()}`)
2985
+ }
2986
+ return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
2987
+ }
2988
+
2774
2989
  if (!locator.isXPath()) {
2775
2990
  return contextObject
2776
- .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2991
+ .locator(`${locator.simplify()} >> text=${text}`)
2777
2992
  .first()
2778
2993
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2994
+ .catch(e => {
2995
+ throw new Error(errorMessage)
2996
+ })
2779
2997
  }
2780
2998
 
2781
2999
  if (locator.isXPath()) {
2782
- return contextObject.waitForFunction(
2783
- ([locator, text, $XPath]) => {
2784
- eval($XPath)
2785
- const el = $XPath(null, locator)
2786
- if (!el.length) return false
2787
- return el[0].innerText.indexOf(text) > -1
2788
- },
2789
- [locator.value, text, $XPath.toString()],
2790
- { timeout: waitTimeout },
2791
- )
3000
+ return contextObject
3001
+ .waitForFunction(
3002
+ ([locator, text, $XPath]) => {
3003
+ eval($XPath)
3004
+ const el = $XPath(null, locator)
3005
+ if (!el.length) return false
3006
+ return el[0].innerText.indexOf(text) > -1
3007
+ },
3008
+ [locator.value, text, $XPath.toString()],
3009
+ { timeout: waitTimeout },
3010
+ )
3011
+ .catch(e => {
3012
+ throw new Error(errorMessage)
3013
+ })
2792
3014
  }
2793
3015
  } catch (e) {
2794
3016
  throw new Error(`${errorMessage}\n${e.message}`)
2795
3017
  }
2796
3018
  }
2797
3019
 
3020
+ // Based on original implementation but fixed to check title text and remove problematic promiseRetry
3021
+ // Original used timeoutGap for waitForFunction to give it slightly more time than the locator
2798
3022
  const timeoutGap = waitTimeout + 1000
2799
3023
 
2800
- // We add basic timeout to make sure we don't wait forever
2801
- // We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
2802
- // or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
2803
- // If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
2804
3024
  return Promise.race([
2805
- new Promise((_, reject) => {
2806
- setTimeout(() => reject(errorMessage), waitTimeout)
2807
- }),
2808
- this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2809
- promiseRetry(
2810
- async retry => {
2811
- const textPresent = await contextObject
2812
- .locator(`:has-text(${JSON.stringify(text)})`)
2813
- .first()
2814
- .isVisible()
2815
- if (!textPresent) retry(errorMessage)
3025
+ // Strategy 1: waitForFunction that checks both body AND title text
3026
+ // Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
3027
+ // Original only checked document.body.innerText, missing title text like "TestEd"
3028
+ this.page.waitForFunction(
3029
+ function (text) {
3030
+ // Check body text (original behavior)
3031
+ if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
3032
+ return true
3033
+ }
3034
+ // Check document title (fixes the TestEd in title issue)
3035
+ if (document.title && document.title.indexOf(text) > -1) {
3036
+ return true
3037
+ }
3038
+ return false
2816
3039
  },
2817
- { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
3040
+ text,
3041
+ { timeout: timeoutGap },
2818
3042
  ),
2819
- ])
3043
+ // Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
3044
+ contextObject
3045
+ .locator(`:has-text(${JSON.stringify(text)})`)
3046
+ .first()
3047
+ .waitFor({ timeout: waitTimeout }),
3048
+ ]).catch(err => {
3049
+ throw new Error(errorMessage)
3050
+ })
2820
3051
  }
2821
3052
 
2822
3053
  /**
@@ -3402,9 +3633,15 @@ class Playwright extends Helper {
3402
3633
 
3403
3634
  module.exports = Playwright
3404
3635
 
3636
+ function buildCustomLocatorString(locator) {
3637
+ // Note: this.debug not available in standalone function, using console.log
3638
+ console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
3639
+ return `${locator.type}=${locator.value}`
3640
+ }
3641
+
3405
3642
  function buildLocatorString(locator) {
3406
3643
  if (locator.isCustom()) {
3407
- return `${locator.type}=${locator.value}`
3644
+ return buildCustomLocatorString(locator)
3408
3645
  }
3409
3646
  if (locator.isXPath()) {
3410
3647
  return `xpath=${locator.value}`
@@ -3416,15 +3653,119 @@ async function findElements(matcher, locator) {
3416
3653
  if (locator.react) return findReact(matcher, locator)
3417
3654
  if (locator.vue) return findVue(matcher, locator)
3418
3655
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3656
+
3419
3657
  locator = new Locator(locator, 'css')
3420
3658
 
3421
- return matcher.locator(buildLocatorString(locator)).all()
3659
+ // Handle custom locators directly instead of relying on Playwright selector engines
3660
+ if (locator.isCustom()) {
3661
+ return findCustomElements.call(this, matcher, locator)
3662
+ }
3663
+
3664
+ // Check if we have a custom context locator and need to search within it
3665
+ if (this.contextLocator) {
3666
+ const contextLocatorObj = new Locator(this.contextLocator, 'css')
3667
+ if (contextLocatorObj.isCustom()) {
3668
+ // Find the context elements first
3669
+ const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
3670
+ if (contextElements.length === 0) {
3671
+ return []
3672
+ }
3673
+
3674
+ // Search within the first context element
3675
+ const locatorString = buildLocatorString(locator)
3676
+ return contextElements[0].locator(locatorString).all()
3677
+ }
3678
+ }
3679
+
3680
+ const locatorString = buildLocatorString(locator)
3681
+
3682
+ return matcher.locator(locatorString).all()
3683
+ }
3684
+
3685
+ async function findCustomElements(matcher, locator) {
3686
+ const customLocatorStrategies = this.customLocatorStrategies || globalCustomLocatorStrategies
3687
+ const strategyFunction = customLocatorStrategies.get ? customLocatorStrategies.get(locator.type) : customLocatorStrategies[locator.type]
3688
+
3689
+ if (!strategyFunction) {
3690
+ throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
3691
+ }
3692
+
3693
+ // Execute the custom locator function in the browser context using page.evaluate
3694
+ const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
3695
+
3696
+ const elements = await page.evaluate(
3697
+ ({ strategyCode, selector }) => {
3698
+ const strategy = new Function('return ' + strategyCode)()
3699
+ const result = strategy(selector, document)
3700
+
3701
+ // Convert NodeList or single element to array
3702
+ if (result && result.nodeType) {
3703
+ return [result]
3704
+ } else if (result && result.length !== undefined) {
3705
+ return Array.from(result)
3706
+ } else if (Array.isArray(result)) {
3707
+ return result
3708
+ }
3709
+
3710
+ return []
3711
+ },
3712
+ {
3713
+ strategyCode: strategyFunction.toString(),
3714
+ selector: locator.value,
3715
+ },
3716
+ )
3717
+
3718
+ // Convert the found elements back to Playwright locators
3719
+ if (elements.length === 0) {
3720
+ return []
3721
+ }
3722
+
3723
+ // Create CSS selectors for the found elements and return as locators
3724
+ const locators = []
3725
+ const timestamp = Date.now()
3726
+
3727
+ for (let i = 0; i < elements.length; i++) {
3728
+ // Use a unique attribute approach to target specific elements
3729
+ const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
3730
+
3731
+ await page.evaluate(
3732
+ ({ index, uniqueAttr, strategyCode, selector }) => {
3733
+ // Re-execute the strategy to find elements and mark the specific one
3734
+ const strategy = new Function('return ' + strategyCode)()
3735
+ const result = strategy(selector, document)
3736
+
3737
+ let elementsArray = []
3738
+ if (result && result.nodeType) {
3739
+ elementsArray = [result]
3740
+ } else if (result && result.length !== undefined) {
3741
+ elementsArray = Array.from(result)
3742
+ } else if (Array.isArray(result)) {
3743
+ elementsArray = result
3744
+ }
3745
+
3746
+ if (elementsArray[index]) {
3747
+ elementsArray[index].setAttribute(uniqueAttr, 'true')
3748
+ }
3749
+ },
3750
+ {
3751
+ index: i,
3752
+ uniqueAttr,
3753
+ strategyCode: strategyFunction.toString(),
3754
+ selector: locator.value,
3755
+ },
3756
+ )
3757
+
3758
+ locators.push(page.locator(`[${uniqueAttr}="true"]`))
3759
+ }
3760
+
3761
+ return locators
3422
3762
  }
3423
3763
 
3424
3764
  async function findElement(matcher, locator) {
3425
3765
  if (locator.react) return findReact(matcher, locator)
3426
3766
  if (locator.vue) return findVue(matcher, locator)
3427
3767
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3768
+
3428
3769
  locator = new Locator(locator, 'css')
3429
3770
 
3430
3771
  return matcher.locator(buildLocatorString(locator)).first()
@@ -3745,9 +4086,7 @@ async function targetCreatedHandler(page) {
3745
4086
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
3746
4087
  try {
3747
4088
  await page.setViewportSize(parseWindowSize(this.options.windowSize))
3748
- } catch (err) {
3749
- this.debug('Target can be already closed, ignoring...')
3750
- }
4089
+ } catch (err) {}
3751
4090
  }
3752
4091
  }
3753
4092