codeceptjs 4.0.1-beta.18 → 4.0.1-beta.19

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
@@ -2489,7 +2489,19 @@ class Playwright extends Helper {
2489
2489
  * {{> selectOption }}
2490
2490
  */
2491
2491
  async selectOption(select, option) {
2492
- 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
+ }
2493
2505
  assertElementExists(els, select, 'Selectable field')
2494
2506
  const el = els[0]
2495
2507
 
@@ -2790,17 +2802,6 @@ class Playwright extends Helper {
2790
2802
  *
2791
2803
  */
2792
2804
  async grabTextFrom(locator) {
2793
- // Handle role locators with text/exact options
2794
- if (isRoleLocatorObject(locator)) {
2795
- const elements = await handleRoleLocator(this.page, locator)
2796
- if (elements && elements.length > 0) {
2797
- const text = await elements[0].textContent()
2798
- assertElementExists(text, JSON.stringify(locator))
2799
- this.debugSection('Text', text)
2800
- return text
2801
- }
2802
- }
2803
-
2804
2805
  const locatorObj = new Locator(locator, 'css')
2805
2806
 
2806
2807
  if (locatorObj.isCustom()) {
@@ -2813,21 +2814,32 @@ class Playwright extends Helper {
2813
2814
  assertElementExists(text, locatorObj.toString())
2814
2815
  this.debugSection('Text', text)
2815
2816
  return text
2816
- } else {
2817
- locator = this._contextLocator(locator)
2818
- try {
2819
- const text = await this.page.textContent(locator)
2820
- 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))
2821
2825
  this.debugSection('Text', text)
2822
2826
  return text
2823
- } catch (error) {
2824
- // Convert Playwright timeout errors to ElementNotFound for consistency
2825
- if (error.message && error.message.includes('Timeout')) {
2826
- throw new ElementNotFound(locator, 'text')
2827
- }
2828
- throw error
2829
2827
  }
2830
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
+ }
2831
2843
  }
2832
2844
 
2833
2845
  /**
@@ -4306,55 +4318,26 @@ function buildLocatorString(locator) {
4306
4318
  if (locator.isXPath()) {
4307
4319
  return `xpath=${locator.value}`
4308
4320
  }
4309
- if (locator.isJson()) {
4310
- // For JSON locators, pass the entire object to Playwright
4311
- // Playwright natively supports role locator objects
4312
- return locator.value
4313
- }
4314
- return locator.simplify()
4315
- }
4316
-
4317
- /**
4318
- * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4319
- */
4320
- function isRoleLocatorObject(locator) {
4321
- return locator && typeof locator === 'object' && locator.role && !locator.type
4321
+ return locator.simplify()
4322
4322
  }
4323
4323
 
4324
- /**
4325
- * Handles role locator objects by converting them to Playwright's getByRole() API
4326
- * Returns elements array if role locator, null otherwise
4327
- */
4328
- async function handleRoleLocator(context, locator) {
4329
- if (!isRoleLocatorObject(locator)) return null
4330
-
4331
- const options = {}
4332
- if (locator.text) options.name = locator.text
4333
- if (locator.exact !== undefined) options.exact = locator.exact
4334
-
4335
- 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()
4336
4329
  }
4337
4330
 
4338
4331
  async function findElements(matcher, locator) {
4339
- // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4340
- const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4341
- const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4342
- const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4343
-
4344
- if (isReactLocator) return findReact(matcher, locator)
4345
- if (isVueLocator) return findVue(matcher, locator)
4346
- if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4347
-
4348
- // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
4349
- const roleElements = await handleRoleLocator(matcher, locator)
4332
+ const matchedLocator = Locator.from(locator)
4333
+ const roleElements = await findByRole(matcher, matchedLocator)
4350
4334
  if (roleElements) return roleElements
4351
4335
 
4352
- locator = new Locator(locator, 'css')
4336
+ const isReactLocator = matchedLocator.type === 'react'
4337
+ const isVueLocator = matchedLocator.type === 'vue'
4353
4338
 
4354
- // Handle custom locators directly instead of relying on Playwright selector engines
4355
- if (locator.isCustom()) {
4356
- return findCustomElements.call(this, matcher, locator)
4357
- }
4339
+ if (isReactLocator) return findReact(matcher, matchedLocator)
4340
+ if (isVueLocator) return findVue(matcher, matchedLocator)
4358
4341
 
4359
4342
  // Check if we have a custom context locator and need to search within it
4360
4343
  if (this.contextLocator) {
@@ -4367,12 +4350,12 @@ async function findElements(matcher, locator) {
4367
4350
  }
4368
4351
 
4369
4352
  // Search within the first context element
4370
- const locatorString = buildLocatorString(locator)
4353
+ const locatorString = buildLocatorString(matchedLocator)
4371
4354
  return contextElements[0].locator(locatorString).all()
4372
4355
  }
4373
4356
  }
4374
4357
 
4375
- const locatorString = buildLocatorString(locator)
4358
+ const locatorString = buildLocatorString(matchedLocator)
4376
4359
 
4377
4360
  return matcher.locator(locatorString).all()
4378
4361
  }
@@ -4465,13 +4448,17 @@ async function findCustomElements(matcher, locator) {
4465
4448
  }
4466
4449
 
4467
4450
  async function findElement(matcher, locator) {
4468
- if (locator.react) return findReact(matcher, locator)
4469
- if (locator.vue) return findVue(matcher, locator)
4470
- 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'
4471
4457
 
4472
- locator = new Locator(locator, 'css')
4458
+ if (isReactLocator) return findReact(matcher, matchedLocator)
4459
+ if (isVueLocator) return findVue(matcher, matchedLocator)
4473
4460
 
4474
- return matcher.locator(buildLocatorString(locator)).first()
4461
+ return matcher.locator(buildLocatorString(matchedLocator)).first()
4475
4462
  }
4476
4463
 
4477
4464
  async function getVisibleElements(elements) {
@@ -4523,8 +4510,14 @@ async function proceedClick(locator, context = null, options = {}) {
4523
4510
  }
4524
4511
 
4525
4512
  async function findClickable(matcher, locator) {
4513
+ // Convert to Locator first to handle JSON strings properly
4526
4514
  const matchedLocator = new Locator(locator)
4527
4515
 
4516
+ // Handle role locators from Locator
4517
+ if (matchedLocator.isRole()) {
4518
+ return findByRole(matcher, matchedLocator)
4519
+ }
4520
+
4528
4521
  if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
4529
4522
 
4530
4523
  let els
@@ -4597,14 +4590,17 @@ async function findCheckable(locator, context) {
4597
4590
  }
4598
4591
 
4599
4592
  // Handle role locators with text/exact options
4600
- const roleElements = await handleRoleLocator(contextEl, locator)
4593
+ const roleElements = await findByRole(contextEl, locator)
4601
4594
  if (roleElements) return roleElements
4602
4595
 
4603
- const matchedLocator = new Locator(locator)
4596
+ const matchedLocator = Locator.from(locator)
4604
4597
  if (!matchedLocator.isFuzzy()) {
4605
4598
  return findElements.call(this, contextEl, matchedLocator)
4606
4599
  }
4607
4600
 
4601
+ const checkboxByRole = await findByRole(contextEl, { role: 'checkbox', name: matchedLocator.value })
4602
+ if (checkboxByRole) return checkboxByRole
4603
+
4608
4604
  const literal = xpathLocator.literal(matchedLocator.value)
4609
4605
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
4610
4606
  if (els.length) {
@@ -4626,17 +4622,15 @@ async function proceedIsChecked(assertType, option) {
4626
4622
  }
4627
4623
 
4628
4624
  async function findFields(locator) {
4629
- // Handle role locators with text/exact options
4630
- if (isRoleLocatorObject(locator)) {
4631
- const page = await this.page
4632
- const roleElements = await handleRoleLocator(page, locator)
4633
- if (roleElements) return roleElements
4634
- }
4625
+ const page = await this.page
4626
+ const roleElements = await findByRole(page, locator)
4627
+ if (roleElements) return roleElements
4635
4628
 
4636
4629
  const matchedLocator = new Locator(locator)
4637
4630
  if (!matchedLocator.isFuzzy()) {
4638
4631
  return this._locate(matchedLocator)
4639
4632
  }
4633
+
4640
4634
  const literal = xpathLocator.literal(locator)
4641
4635
 
4642
4636
  let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
@@ -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 }
package/lib/locator.js CHANGED
@@ -5,7 +5,7 @@ import { createRequire } from 'module'
5
5
  const require = createRequire(import.meta.url)
6
6
  let cssToXPath
7
7
 
8
- const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'pw', 'role', 'json']
8
+ const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'role']
9
9
  /** @class */
10
10
  class Locator {
11
11
  /**
@@ -29,14 +29,7 @@ class Locator {
29
29
  Object.assign(this, locator)
30
30
  return
31
31
  }
32
-
33
- this.locator = locator
34
- this.type = Object.keys(locator)[0]
35
- this.value = locator[this.type]
36
- this.strict = true
37
-
38
- Locator.filters.forEach(f => f(locator, this))
39
-
32
+ this._applyObjectLocator(locator)
40
33
  return
41
34
  }
42
35
 
@@ -58,8 +51,9 @@ class Locator {
58
51
  if (isShadow(locator)) {
59
52
  this.type = 'shadow'
60
53
  }
61
- if (isPlaywrightLocator(locator)) {
62
- this.type = 'pw'
54
+ if (isReactVueLocator(locator)) {
55
+ // React/Vue locators - keep as fuzzy type, helpers will handle them specially
56
+ this.type = 'fuzzy'
63
57
  }
64
58
 
65
59
  Locator.filters.forEach(f => f(locator, this))
@@ -81,18 +75,17 @@ class Locator {
81
75
  return this.value
82
76
  case 'shadow':
83
77
  return { shadow: this.value }
84
- case 'pw':
85
- return { pw: this.value }
86
78
  case 'role':
87
79
  return `[role="${this.value}"]`
88
- case 'json':
89
- return { json: this.value }
90
80
  }
91
81
  return this.value
92
82
  }
93
83
 
94
84
  toStrict() {
95
85
  if (!this.type) return null
86
+ if (this.type === 'role' && this.locator) {
87
+ return this.locator
88
+ }
96
89
  return { [this.type]: this.value }
97
90
  }
98
91
 
@@ -109,31 +102,24 @@ class Locator {
109
102
  try {
110
103
  const parsed = JSON.parse(trimmed)
111
104
  if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
112
- this.locator = parsed
113
-
114
- // Check if this is a multi-property JSON (like aria locators)
115
- const keys = Object.keys(parsed)
116
- if (keys.length > 1) {
117
- // For multi-property objects, treat the entire JSON as the value
118
- // with a special type to preserve all properties
119
- this.type = 'json'
120
- this.value = parsed
121
- } else {
122
- // Single property - use existing logic
123
- this.type = keys[0]
124
- this.value = parsed[keys[0]]
125
- }
126
-
127
- this.strict = true
128
- Locator.filters.forEach(f => f(parsed, this))
105
+ this._applyObjectLocator(parsed)
129
106
  return true
130
107
  }
131
108
  } catch (e) {
132
- // continue with normal string processing
133
109
  }
134
110
  return false
135
111
  }
136
112
 
113
+ _applyObjectLocator(locator) {
114
+ this.strict = true
115
+ this.locator = locator
116
+ const keys = Object.keys(locator)
117
+ const [type] = keys
118
+ this.type = type
119
+ this.value = keys.length > 1 ? locator : locator[type]
120
+ Locator.filters.forEach(f => f(locator, this))
121
+ }
122
+
137
123
  /**
138
124
  * @returns {string}
139
125
  */
@@ -169,13 +155,6 @@ class Locator {
169
155
  return this.type === 'css'
170
156
  }
171
157
 
172
- /**
173
- * @returns {boolean}
174
- */
175
- isPlaywrightLocator() {
176
- return this.type === 'pw'
177
- }
178
-
179
158
  /**
180
159
  * @returns {boolean}
181
160
  */
@@ -184,12 +163,22 @@ class Locator {
184
163
  }
185
164
 
186
165
  /**
187
- * @returns {boolean}
166
+ * @returns {{role: string, options: object}|null}
188
167
  */
189
- isJson() {
190
- return this.type === 'json'
168
+ getRoleOptions() {
169
+ if (!this.isRole()) return null
170
+ const data = this.locator && typeof this.locator === 'object' ? this.locator : { role: this.value }
171
+ const { role, text, name, exact, includeHidden, ...rest } = data
172
+ let options = { ...rest }
173
+ const accessibleName = name ?? text
174
+ if (accessibleName !== undefined) options.name = accessibleName
175
+ if (exact !== undefined) options.exact = exact
176
+ if (includeHidden !== undefined) options.includeHidden = includeHidden
177
+ if (Object.keys(options).length === 0) options = undefined
178
+ return { role, options }
191
179
  }
192
180
 
181
+
193
182
  /**
194
183
  * @returns {boolean}
195
184
  */
@@ -456,6 +445,16 @@ Locator.build = locator => {
456
445
  return new Locator(locator, 'css')
457
446
  }
458
447
 
448
+ /**
449
+ * @param {CodeceptJS.LocatorOrString|Locator} locator
450
+ * @param {string} [defaultType]
451
+ * @returns {Locator}
452
+ */
453
+ Locator.from = (locator, defaultType = '') => {
454
+ if (locator instanceof Locator) return locator
455
+ return new Locator(locator, defaultType)
456
+ }
457
+
459
458
  /**
460
459
  * Filters to modify locators
461
460
  * @type {Array<function(CodeceptJS.LocatorOrString, Locator): void>}
@@ -656,20 +655,12 @@ function removePrefix(xpath) {
656
655
  * @param {string} locator
657
656
  * @returns {boolean}
658
657
  */
659
- function isPlaywrightLocator(locator) {
658
+ function isReactVueLocator(locator) {
660
659
  return locator.includes('_react') || locator.includes('_vue')
661
660
  }
662
661
 
663
662
  /**
664
663
  * @private
665
- * check if the locator is a role locator
666
- * @param {{role: string}} locator
667
- * @returns {boolean}
668
- */
669
- function isRoleLocator(locator) {
670
- return locator.role !== undefined && typeof locator.role === 'string' && Object.keys(locator).length >= 1
671
- }
672
-
673
664
  /**
674
665
  * @private
675
666
  * @param {CodeceptJS.LocatorOrString} locator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.1-beta.18",
3
+ "version": "4.0.1-beta.19",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [