codeceptjs 4.0.1-beta.2 → 4.0.1-beta.20

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.
@@ -30,7 +30,7 @@ import ElementNotFound from './errors/ElementNotFound.js'
30
30
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
31
31
  import Popup from './extras/Popup.js'
32
32
  import Console from './extras/Console.js'
33
- import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
33
+ import { findReact, findVue } from './extras/PlaywrightLocator.js'
34
34
  import WebElement from '../element/WebElement.js'
35
35
 
36
36
  let playwright
@@ -355,7 +355,7 @@ class Playwright extends Helper {
355
355
  this.recordingWebSocketMessages = false
356
356
  this.recordedWebSocketMessagesAtLeastOnce = false
357
357
  this.cdpSession = null
358
-
358
+
359
359
  // Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
360
360
  // This can happen in worker threads where config is serialized/deserialized
361
361
  let validCustomLocators = null
@@ -367,7 +367,7 @@ class Playwright extends Helper {
367
367
  validCustomLocators = config.customLocatorStrategies
368
368
  }
369
369
  }
370
-
370
+
371
371
  this.customLocatorStrategies = validCustomLocators
372
372
  this._customLocatorsRegistered = false
373
373
 
@@ -416,6 +416,7 @@ class Playwright extends Helper {
416
416
  ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
417
417
  highlightElement: false,
418
418
  storageState: undefined,
419
+ onResponse: null,
419
420
  }
420
421
 
421
422
  process.env.testIdAttribute = 'data-testid'
@@ -794,10 +795,7 @@ class Playwright extends Helper {
794
795
  await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
795
796
  }
796
797
  // Use timeout to prevent hanging (10s should be enough for browser cleanup)
797
- await Promise.race([
798
- this._stopBrowser(),
799
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
800
- ])
798
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
801
799
  } catch (e) {
802
800
  console.warn('Warning during browser restart in _after:', e.message)
803
801
  // Force cleanup even on timeout
@@ -840,10 +838,7 @@ class Playwright extends Helper {
840
838
  if (this.isRunning) {
841
839
  try {
842
840
  // Add timeout protection to prevent hanging
843
- await Promise.race([
844
- this._stopBrowser(),
845
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
846
- ])
841
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
847
842
  } catch (e) {
848
843
  console.warn('Warning during suite cleanup:', e.message)
849
844
  // Track suite cleanup failures
@@ -954,10 +949,7 @@ class Playwright extends Helper {
954
949
  if (this.isRunning) {
955
950
  try {
956
951
  // Add timeout protection to prevent hanging
957
- await Promise.race([
958
- this._stopBrowser(),
959
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
960
- ])
952
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
961
953
  } catch (e) {
962
954
  console.warn('Warning during final cleanup:', e.message)
963
955
  // Force cleanup on timeout
@@ -970,10 +962,7 @@ class Playwright extends Helper {
970
962
  if (this.browser) {
971
963
  try {
972
964
  // Add timeout protection to prevent hanging
973
- await Promise.race([
974
- this._stopBrowser(),
975
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
976
- ])
965
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
977
966
  } catch (e) {
978
967
  console.warn('Warning during forced cleanup:', e.message)
979
968
  // Force cleanup on timeout
@@ -1390,7 +1379,7 @@ class Playwright extends Helper {
1390
1379
  this.context = null
1391
1380
  this.frame = null
1392
1381
  popupStore.clear()
1393
-
1382
+
1394
1383
  // Remove all event listeners to prevent hanging
1395
1384
  if (this.browser) {
1396
1385
  try {
@@ -1399,7 +1388,8 @@ class Playwright extends Helper {
1399
1388
  // Ignore errors if browser is already closed
1400
1389
  }
1401
1390
  }
1402
-
1391
+
1392
+ // Close browserContext if recordHar is enabled
1403
1393
  if (this.options.recordHar && this.browserContext) {
1404
1394
  try {
1405
1395
  await this.browserContext.close()
@@ -1408,22 +1398,17 @@ class Playwright extends Helper {
1408
1398
  }
1409
1399
  }
1410
1400
  this.browserContext = null
1411
-
1401
+
1402
+ // Initiate browser close without waiting for it to complete
1403
+ // The browser process will be cleaned up when the Node process exits
1412
1404
  if (this.browser) {
1413
1405
  try {
1414
- // Add timeout to prevent browser.close() from hanging indefinitely
1415
- await Promise.race([
1416
- this.browser.close(),
1417
- new Promise((_, reject) =>
1418
- setTimeout(() => reject(new Error('Browser close timeout')), 5000)
1419
- )
1420
- ])
1406
+ // Fire and forget - don't wait for close to complete
1407
+ this.browser.close().catch(() => {
1408
+ // Silently ignore any errors during async close
1409
+ })
1421
1410
  } catch (e) {
1422
- // Ignore errors if browser is already closed or timeout
1423
- if (!e.message?.includes('Browser close timeout')) {
1424
- // Non-timeout error, can be ignored as well
1425
- }
1426
- // Force cleanup even on error
1411
+ // Ignore any synchronous errors
1427
1412
  }
1428
1413
  }
1429
1414
  this.browser = null
@@ -1539,7 +1524,7 @@ class Playwright extends Helper {
1539
1524
  acceptDownloads: true,
1540
1525
  ...this.options.emulate,
1541
1526
  }
1542
-
1527
+
1543
1528
  try {
1544
1529
  this.browserContext = await this.browser.newContext(contextOptions)
1545
1530
  } catch (err) {
@@ -2504,7 +2489,19 @@ class Playwright extends Helper {
2504
2489
  * {{> selectOption }}
2505
2490
  */
2506
2491
  async selectOption(select, option) {
2507
- const els = await findFields.call(this, select)
2492
+ const selectLocator = Locator.from(select, 'css')
2493
+ let els = null
2494
+
2495
+ if (selectLocator.isFuzzy()) {
2496
+ els = await findByRole(this.page, { role: 'listbox', name: selectLocator.value })
2497
+ if (!els || els.length === 0) {
2498
+ els = await findByRole(this.page, { role: 'combobox', name: selectLocator.value })
2499
+ }
2500
+ }
2501
+
2502
+ if (!els || els.length === 0) {
2503
+ els = await findFields.call(this, select)
2504
+ }
2508
2505
  assertElementExists(els, select, 'Selectable field')
2509
2506
  const el = els[0]
2510
2507
 
@@ -2805,17 +2802,6 @@ class Playwright extends Helper {
2805
2802
  *
2806
2803
  */
2807
2804
  async grabTextFrom(locator) {
2808
- // Handle role locators with text/exact options
2809
- if (isRoleLocatorObject(locator)) {
2810
- const elements = await handleRoleLocator(this.page, locator)
2811
- if (elements && elements.length > 0) {
2812
- const text = await elements[0].textContent()
2813
- assertElementExists(text, JSON.stringify(locator))
2814
- this.debugSection('Text', text)
2815
- return text
2816
- }
2817
- }
2818
-
2819
2805
  const locatorObj = new Locator(locator, 'css')
2820
2806
 
2821
2807
  if (locatorObj.isCustom()) {
@@ -2828,21 +2814,32 @@ class Playwright extends Helper {
2828
2814
  assertElementExists(text, locatorObj.toString())
2829
2815
  this.debugSection('Text', text)
2830
2816
  return text
2831
- } else {
2832
- locator = this._contextLocator(locator)
2833
- try {
2834
- const text = await this.page.textContent(locator)
2835
- assertElementExists(text, locator)
2817
+ }
2818
+
2819
+ if (locatorObj.isRole()) {
2820
+ // Handle role locators with text/exact options
2821
+ const roleElements = await findByRole(this.page, locator)
2822
+ if (roleElements && roleElements.length > 0) {
2823
+ const text = await roleElements[0].textContent()
2824
+ assertElementExists(text, JSON.stringify(locator))
2836
2825
  this.debugSection('Text', text)
2837
2826
  return text
2838
- } catch (error) {
2839
- // Convert Playwright timeout errors to ElementNotFound for consistency
2840
- if (error.message && error.message.includes('Timeout')) {
2841
- throw new ElementNotFound(locator, 'text')
2842
- }
2843
- throw error
2844
2827
  }
2845
2828
  }
2829
+
2830
+ locator = this._contextLocator(locator)
2831
+ try {
2832
+ const text = await this.page.textContent(locator)
2833
+ assertElementExists(text, locator)
2834
+ this.debugSection('Text', text)
2835
+ return text
2836
+ } catch (error) {
2837
+ // Convert Playwright timeout errors to ElementNotFound for consistency
2838
+ if (error.message && error.message.includes('Timeout')) {
2839
+ throw new ElementNotFound(locator, 'text')
2840
+ }
2841
+ throw error
2842
+ }
2846
2843
  }
2847
2844
 
2848
2845
  /**
@@ -3183,14 +3180,14 @@ class Playwright extends Helper {
3183
3180
  this.debugSection('Response', await response.text())
3184
3181
 
3185
3182
  // hook to allow JSON response handle this
3186
- if (this.config.onResponse) {
3183
+ if (this.options.onResponse) {
3187
3184
  const axiosResponse = {
3188
3185
  data: await response.json(),
3189
3186
  status: response.status(),
3190
3187
  statusText: response.statusText(),
3191
3188
  headers: response.headers(),
3192
3189
  }
3193
- this.config.onResponse(axiosResponse)
3190
+ this.options.onResponse(axiosResponse)
3194
3191
  }
3195
3192
 
3196
3193
  return response
@@ -4321,50 +4318,26 @@ function buildLocatorString(locator) {
4321
4318
  if (locator.isXPath()) {
4322
4319
  return `xpath=${locator.value}`
4323
4320
  }
4324
- return locator.simplify()
4321
+ return locator.simplify()
4325
4322
  }
4326
4323
 
4327
- /**
4328
- * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4329
- */
4330
- function isRoleLocatorObject(locator) {
4331
- return locator && typeof locator === 'object' && locator.role && !locator.type
4332
- }
4333
-
4334
- /**
4335
- * Handles role locator objects by converting them to Playwright's getByRole() API
4336
- * Returns elements array if role locator, null otherwise
4337
- */
4338
- async function handleRoleLocator(context, locator) {
4339
- if (!isRoleLocatorObject(locator)) return null
4340
-
4341
- const options = {}
4342
- if (locator.text) options.name = locator.text
4343
- if (locator.exact !== undefined) options.exact = locator.exact
4344
-
4345
- return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4324
+ async function findByRole(context, locator) {
4325
+ const matchedLocator = Locator.from(locator)
4326
+ if (!matchedLocator.isRole()) return null
4327
+ const roleOptions = matchedLocator.getRoleOptions()
4328
+ return context.getByRole(roleOptions.role, roleOptions.options).all()
4346
4329
  }
4347
4330
 
4348
4331
  async function findElements(matcher, locator) {
4349
- // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4350
- const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4351
- const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4352
- const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4353
-
4354
- if (isReactLocator) return findReact(matcher, locator)
4355
- if (isVueLocator) return findVue(matcher, locator)
4356
- if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4357
-
4358
- // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
4359
- const roleElements = await handleRoleLocator(matcher, locator)
4332
+ const matchedLocator = Locator.from(locator)
4333
+ const roleElements = await findByRole(matcher, matchedLocator)
4360
4334
  if (roleElements) return roleElements
4361
4335
 
4362
- locator = new Locator(locator, 'css')
4336
+ const isReactLocator = matchedLocator.type === 'react'
4337
+ const isVueLocator = matchedLocator.type === 'vue'
4363
4338
 
4364
- // Handle custom locators directly instead of relying on Playwright selector engines
4365
- if (locator.isCustom()) {
4366
- return findCustomElements.call(this, matcher, locator)
4367
- }
4339
+ if (isReactLocator) return findReact(matcher, matchedLocator)
4340
+ if (isVueLocator) return findVue(matcher, matchedLocator)
4368
4341
 
4369
4342
  // Check if we have a custom context locator and need to search within it
4370
4343
  if (this.contextLocator) {
@@ -4377,12 +4350,12 @@ async function findElements(matcher, locator) {
4377
4350
  }
4378
4351
 
4379
4352
  // Search within the first context element
4380
- const locatorString = buildLocatorString(locator)
4353
+ const locatorString = buildLocatorString(matchedLocator)
4381
4354
  return contextElements[0].locator(locatorString).all()
4382
4355
  }
4383
4356
  }
4384
4357
 
4385
- const locatorString = buildLocatorString(locator)
4358
+ const locatorString = buildLocatorString(matchedLocator)
4386
4359
 
4387
4360
  return matcher.locator(locatorString).all()
4388
4361
  }
@@ -4391,7 +4364,7 @@ async function findCustomElements(matcher, locator) {
4391
4364
  // Always prioritize this.customLocatorStrategies which is set in constructor from config
4392
4365
  // and persists in every worker thread instance
4393
4366
  let strategyFunction = null
4394
-
4367
+
4395
4368
  if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
4396
4369
  strategyFunction = this.customLocatorStrategies[locator.type]
4397
4370
  } else if (globalCustomLocatorStrategies.has(locator.type)) {
@@ -4475,13 +4448,17 @@ async function findCustomElements(matcher, locator) {
4475
4448
  }
4476
4449
 
4477
4450
  async function findElement(matcher, locator) {
4478
- if (locator.react) return findReact(matcher, locator)
4479
- if (locator.vue) return findVue(matcher, locator)
4480
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4451
+ const matchedLocator = Locator.from(locator)
4452
+ const roleElements = await findByRole(matcher, matchedLocator)
4453
+ if (roleElements && roleElements.length > 0) return roleElements[0]
4454
+
4455
+ const isReactLocator = matchedLocator.type === 'react'
4456
+ const isVueLocator = matchedLocator.type === 'vue'
4481
4457
 
4482
- locator = new Locator(locator, 'css')
4458
+ if (isReactLocator) return findReact(matcher, matchedLocator)
4459
+ if (isVueLocator) return findVue(matcher, matchedLocator)
4483
4460
 
4484
- return matcher.locator(buildLocatorString(locator)).first()
4461
+ return matcher.locator(buildLocatorString(matchedLocator)).first()
4485
4462
  }
4486
4463
 
4487
4464
  async function getVisibleElements(elements) {
@@ -4533,8 +4510,14 @@ async function proceedClick(locator, context = null, options = {}) {
4533
4510
  }
4534
4511
 
4535
4512
  async function findClickable(matcher, locator) {
4513
+ // Convert to Locator first to handle JSON strings properly
4536
4514
  const matchedLocator = new Locator(locator)
4537
4515
 
4516
+ // Handle role locators from Locator
4517
+ if (matchedLocator.isRole()) {
4518
+ return findByRole(matcher, matchedLocator)
4519
+ }
4520
+
4538
4521
  if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
4539
4522
 
4540
4523
  let els
@@ -4607,14 +4590,17 @@ async function findCheckable(locator, context) {
4607
4590
  }
4608
4591
 
4609
4592
  // Handle role locators with text/exact options
4610
- const roleElements = await handleRoleLocator(contextEl, locator)
4593
+ const roleElements = await findByRole(contextEl, locator)
4611
4594
  if (roleElements) return roleElements
4612
4595
 
4613
- const matchedLocator = new Locator(locator)
4596
+ const matchedLocator = Locator.from(locator)
4614
4597
  if (!matchedLocator.isFuzzy()) {
4615
4598
  return findElements.call(this, contextEl, matchedLocator)
4616
4599
  }
4617
4600
 
4601
+ const checkboxByRole = await findByRole(contextEl, { role: 'checkbox', name: matchedLocator.value })
4602
+ if (checkboxByRole) return checkboxByRole
4603
+
4618
4604
  const literal = xpathLocator.literal(matchedLocator.value)
4619
4605
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
4620
4606
  if (els.length) {
@@ -4636,17 +4622,15 @@ async function proceedIsChecked(assertType, option) {
4636
4622
  }
4637
4623
 
4638
4624
  async function findFields(locator) {
4639
- // Handle role locators with text/exact options
4640
- if (isRoleLocatorObject(locator)) {
4641
- const page = await this.page
4642
- const roleElements = await handleRoleLocator(page, locator)
4643
- if (roleElements) return roleElements
4644
- }
4625
+ const page = await this.page
4626
+ const roleElements = await findByRole(page, locator)
4627
+ if (roleElements) return roleElements
4645
4628
 
4646
4629
  const matchedLocator = new Locator(locator)
4647
4630
  if (!matchedLocator.isFuzzy()) {
4648
4631
  return this._locate(matchedLocator)
4649
4632
  }
4633
+
4650
4634
  const literal = xpathLocator.literal(locator)
4651
4635
 
4652
4636
  let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
@@ -4967,7 +4951,7 @@ async function refreshContextSession() {
4967
4951
  this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
4968
4952
  return
4969
4953
  }
4970
-
4954
+
4971
4955
  const currentUrl = await this.grabCurrentUrl()
4972
4956
 
4973
4957
  if (currentUrl.startsWith('http')) {
@@ -94,7 +94,9 @@ const config = {}
94
94
  class REST extends Helper {
95
95
  constructor(config) {
96
96
  super(config)
97
- this.options = {
97
+
98
+ // Set defaults first
99
+ const defaults = {
98
100
  timeout: 10000,
99
101
  defaultHeaders: {},
100
102
  endpoint: '',
@@ -103,15 +105,16 @@ class REST extends Helper {
103
105
  onResponse: null,
104
106
  }
105
107
 
108
+ // Merge config with defaults
109
+ this._setConfig(config)
110
+ this.options = { ...defaults, ...this.options }
111
+
106
112
  if (this.options.maxContentLength) {
107
113
  const maxContentLength = this.options.maxUploadFileSize * 1024 * 1024
108
114
  this.options.maxContentLength = maxContentLength
109
115
  this.options.maxBodyLength = maxContentLength
110
116
  }
111
117
 
112
- // override defaults with config
113
- this._setConfig(config)
114
-
115
118
  this.headers = { ...this.options.defaultHeaders }
116
119
 
117
120
  // Create an agent with SSL certificate
@@ -215,8 +218,9 @@ class REST extends Helper {
215
218
  }
216
219
  }
217
220
 
218
- if (this.config.onRequest) {
219
- await this.config.onRequest(request)
221
+ const onRequest = this.options.onRequest || this.config.onRequest
222
+ if (onRequest) {
223
+ await onRequest(request)
220
224
  }
221
225
 
222
226
  try {
@@ -245,8 +249,9 @@ class REST extends Helper {
245
249
  }
246
250
  response = err.response
247
251
  }
248
- if (this.config.onResponse) {
249
- await this.config.onResponse(response)
252
+ const onResponse = this.options.onResponse || this.config.onResponse
253
+ if (onResponse) {
254
+ await onResponse(response)
250
255
  }
251
256
  try {
252
257
  this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data))
@@ -11,22 +11,20 @@ function buildLocatorString(locator) {
11
11
  }
12
12
 
13
13
  async function findElements(matcher, locator) {
14
- const matchedLocator = new Locator(locator, 'css')
14
+ const matchedLocator = Locator.from(locator, 'css')
15
15
 
16
16
  if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
17
17
  if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
18
- if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator)
19
18
  if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator)
20
19
 
21
20
  return matcher.locator(buildLocatorString(matchedLocator)).all()
22
21
  }
23
22
 
24
23
  async function findElement(matcher, locator) {
25
- const matchedLocator = new Locator(locator, 'css')
24
+ const matchedLocator = Locator.from(locator, 'css')
26
25
 
27
26
  if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
28
27
  if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
29
- if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator, { first: true })
30
28
  if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator, { first: true })
31
29
 
32
30
  return matcher.locator(buildLocatorString(matchedLocator)).first()
@@ -46,49 +44,30 @@ async function getVisibleElements(elements) {
46
44
  }
47
45
 
48
46
  async function findReact(matcher, locator) {
49
- const details = locator.locator ?? { react: locator.value }
50
- let locatorString = `_react=${details.react}`
47
+ const props = locator.locator?.props
48
+ let locatorString = `_react=${locator.value}`
51
49
 
52
- if (details.props) {
53
- locatorString += propBuilder(details.props)
50
+ if (props) {
51
+ locatorString += propBuilder(props)
54
52
  }
55
53
 
56
54
  return matcher.locator(locatorString).all()
57
55
  }
58
56
 
59
57
  async function findVue(matcher, locator) {
60
- const details = locator.locator ?? { vue: locator.value }
61
- let locatorString = `_vue=${details.vue}`
58
+ const props = locator.locator?.props
59
+ let locatorString = `_vue=${locator.value}`
62
60
 
63
- if (details.props) {
64
- locatorString += propBuilder(details.props)
61
+ if (props) {
62
+ locatorString += propBuilder(props)
65
63
  }
66
64
 
67
65
  return matcher.locator(locatorString).all()
68
66
  }
69
67
 
70
- async function findByPlaywrightLocator(matcher, locator, { first = false } = {}) {
71
- const details = locator.locator ?? { pw: locator.value }
72
- const locatorValue = details.pw
73
-
74
- const handle = matcher.locator(locatorValue)
75
- return first ? handle.first() : handle.all()
76
- }
77
-
78
68
  async function findByRole(matcher, locator, { first = false } = {}) {
79
- const details = locator.locator ?? { role: locator.value }
80
- const { role, text, name, exact, includeHidden, ...rest } = details
81
- const options = { ...rest }
82
-
83
- if (includeHidden !== undefined) options.includeHidden = includeHidden
84
-
85
- const accessibleName = name ?? text
86
- if (accessibleName !== undefined) {
87
- options.name = accessibleName
88
- if (exact === true) options.exact = true
89
- }
90
-
91
- const roleLocator = matcher.getByRole(role, options)
69
+ const roleOptions = locator.getRoleOptions()
70
+ const roleLocator = matcher.getByRole(roleOptions.role, roleOptions.options)
92
71
  return first ? roleLocator.first() : roleLocator.all()
93
72
  }
94
73
 
@@ -107,4 +86,4 @@ function propBuilder(props) {
107
86
  return _props
108
87
  }
109
88
 
110
- export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole }
89
+ export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByRole }
@@ -12,15 +12,23 @@ export default function () {
12
12
  return
13
13
  }
14
14
  global.__codeceptConfigListenerInitialized = true
15
-
16
- const helpers = global.container.helpers()
17
15
 
18
16
  enableDynamicConfigFor('suite')
19
17
  enableDynamicConfigFor('test')
20
18
 
21
19
  function enableDynamicConfigFor(type) {
22
20
  event.dispatcher.on(event[type].before, (context = {}) => {
21
+ // Get helpers dynamically at runtime, not at initialization time
22
+ // This ensures we get the actual helper instances, not placeholders
23
+ const helpers = global.container.helpers()
24
+
23
25
  function updateHelperConfig(helper, config) {
26
+ // Guard against undefined or invalid helpers
27
+ if (!helper || !helper.constructor) {
28
+ output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`)
29
+ return
30
+ }
31
+
24
32
  const oldConfig = deepClone(helper.options)
25
33
  try {
26
34
  helper._setConfig(deepMerge(deepClone(oldConfig), config))
@@ -41,7 +49,7 @@ export default function () {
41
49
  for (let name in context.config) {
42
50
  const config = context.config[name]
43
51
  if (name === '0') {
44
- // first helper
52
+ // first helper - get dynamically
45
53
  name = Object.keys(helpers)[0]
46
54
  }
47
55
  const helper = helpers[name]