codeceptjs 4.0.1-beta.23 → 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.
@@ -1617,30 +1617,33 @@ class Puppeteer extends Helper {
1617
1617
  * {{> selectOption }}
1618
1618
  */
1619
1619
  async selectOption(select, option) {
1620
- const context = await this._getContext()
1621
- const matchedLocator = new Locator(select)
1622
-
1623
- // Strict locator
1624
- if (!matchedLocator.isFuzzy()) {
1625
- this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1626
- const els = await this._locate(matchedLocator)
1627
- assertElementExists(els, select, 'Selectable element')
1628
- return proceedSelect.call(this, context, els[0], option)
1620
+ const els = await findVisibleFields.call(this, select)
1621
+ assertElementExists(els, select, 'Selectable field')
1622
+ const el = els[0]
1623
+ if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
1624
+ throw new Error('Element is not <select>')
1629
1625
  }
1626
+ highlightActiveElement.call(this, els[0], await this._getContext())
1627
+ if (!Array.isArray(option)) option = [option]
1628
+
1629
+ for (const key in option) {
1630
+ const opt = xpathLocator.literal(option[key])
1631
+ let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
1632
+ if (optEl.length) {
1633
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1634
+ continue
1635
+ }
1636
+ optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
1637
+ if (optEl.length) {
1638
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1639
+ }
1640
+ }
1641
+ await this._evaluateHandeInContext(element => {
1642
+ element.dispatchEvent(new Event('input', { bubbles: true }))
1643
+ element.dispatchEvent(new Event('change', { bubbles: true }))
1644
+ }, el)
1630
1645
 
1631
- // Fuzzy: try combobox
1632
- this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1633
- let els = await findByRole.call(this, context, { role: 'combobox', name: matchedLocator.value })
1634
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1635
-
1636
- // Fuzzy: try listbox
1637
- els = await findByRole.call(this, context, { role: 'listbox', name: matchedLocator.value })
1638
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1639
-
1640
- // Fuzzy: try native select
1641
- els = await findVisibleFields.call(this, select)
1642
- assertElementExists(els, select, 'Selectable element')
1643
- return proceedSelect.call(this, context, els[0], option)
1646
+ return this._waitForAction()
1644
1647
  }
1645
1648
 
1646
1649
  /**
@@ -2952,7 +2955,7 @@ async function findElements(matcher, locator) {
2952
2955
  async function findElement(matcher, locator) {
2953
2956
  if (locator.react) return findReactElements.call(this, locator)
2954
2957
  locator = new Locator(locator, 'css')
2955
-
2958
+
2956
2959
  // Check if locator is a role locator and call findByRole
2957
2960
  if (locator.isRole()) {
2958
2961
  const elements = await findByRole.call(this, matcher, locator)
@@ -2964,10 +2967,13 @@ async function findElement(matcher, locator) {
2964
2967
  const elements = await matcher.$$(locator.simplify())
2965
2968
  return elements[0]
2966
2969
  }
2967
-
2968
- // For XPath in Puppeteer 24.x+, use the same approach as findElements
2969
- // $x method was removed, so we use ::-p-xpath() or fallback
2970
- const elements = await findElements.call(this, matcher, locator)
2970
+ // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2971
+ if (puppeteer.default?.defaultBrowserRevision) {
2972
+ const elements = await matcher.$$(`xpath/${locator.value}`)
2973
+ return elements[0]
2974
+ }
2975
+ // For Puppeteer 24.x+, $x method was removed - use ::-p-xpath() selector
2976
+ const elements = await matcher.$$(`::-p-xpath(${locator.value})`)
2971
2977
  return elements[0]
2972
2978
  }
2973
2979
 
@@ -3157,90 +3163,6 @@ async function findFields(locator) {
3157
3163
  return this._locate({ css: matchedLocator.value })
3158
3164
  }
3159
3165
 
3160
- async function proceedSelect(context, el, option) {
3161
- const role = await el.evaluate(e => e.getAttribute('role'))
3162
- const options = Array.isArray(option) ? option : [option]
3163
-
3164
- if (role === 'combobox') {
3165
- this.debugSection('SelectOption', 'Expanding combobox')
3166
- highlightActiveElement.call(this, el, context)
3167
- const [ariaOwns, ariaControls] = await el.evaluate(e => [e.getAttribute('aria-owns'), e.getAttribute('aria-controls')])
3168
- await el.click()
3169
- await this._waitForAction()
3170
-
3171
- const listboxId = ariaOwns || ariaControls
3172
- let listbox = listboxId ? await context.$(`#${listboxId}`) : null
3173
- if (!listbox) {
3174
- const listboxes = await context.$$('::-p-aria([role="listbox"])')
3175
- listbox = listboxes[0]
3176
- }
3177
- if (!listbox) throw new Error('Cannot find listbox for combobox')
3178
-
3179
- for (const opt of options) {
3180
- const optionEls = await listbox.$$('::-p-aria([role="option"])')
3181
- let optEl = null
3182
- for (const optionEl of optionEls) {
3183
- const text = await optionEl.evaluate(e => e.textContent.trim())
3184
- if (text === opt || text.includes(opt)) {
3185
- optEl = optionEl
3186
- break
3187
- }
3188
- }
3189
- if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3190
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
3191
- highlightActiveElement.call(this, optEl, context)
3192
- await optEl.click()
3193
- }
3194
- return this._waitForAction()
3195
- }
3196
-
3197
- if (role === 'listbox') {
3198
- highlightActiveElement.call(this, el, context)
3199
- for (const opt of options) {
3200
- const optionEls = await el.$$('::-p-aria([role="option"])')
3201
- let optEl = null
3202
- for (const optionEl of optionEls) {
3203
- const text = await optionEl.evaluate(e => e.textContent.trim())
3204
- if (text === opt || text.includes(opt)) {
3205
- optEl = optionEl
3206
- break
3207
- }
3208
- }
3209
- if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3210
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
3211
- highlightActiveElement.call(this, optEl, context)
3212
- await optEl.click()
3213
- }
3214
- return this._waitForAction()
3215
- }
3216
-
3217
- // Native <select> element
3218
- const tagName = await el.evaluate(e => e.tagName)
3219
- if (tagName !== 'SELECT') {
3220
- throw new Error('Element is not <select>')
3221
- }
3222
-
3223
- highlightActiveElement.call(this, el, context)
3224
- for (const key in options) {
3225
- const opt = xpathLocator.literal(options[key])
3226
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
3227
- if (optEl.length) {
3228
- this._evaluateHandeInContext(e => (e.selected = true), optEl[0])
3229
- continue
3230
- }
3231
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
3232
- if (optEl.length) {
3233
- this._evaluateHandeInContext(e => (e.selected = true), optEl[0])
3234
- }
3235
- }
3236
- await this._evaluateHandeInContext(element => {
3237
- element.dispatchEvent(new Event('input', { bubbles: true }))
3238
- element.dispatchEvent(new Event('change', { bubbles: true }))
3239
- }, el)
3240
-
3241
- return this._waitForAction()
3242
- }
3243
-
3244
3166
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3245
3167
  const src = await this._locateElement(sourceLocator)
3246
3168
  if (!src) {
@@ -1302,29 +1302,33 @@ class WebDriver extends Helper {
1302
1302
  * {{> selectOption }}
1303
1303
  */
1304
1304
  async selectOption(select, option) {
1305
- const matchedLocator = new Locator(select)
1305
+ const res = await findFields.call(this, select)
1306
+ assertElementExists(res, select, 'Selectable field')
1307
+ const elem = usingFirstElement(res)
1308
+ highlightActiveElement.call(this, elem)
1306
1309
 
1307
- // Strict locator
1308
- if (!matchedLocator.isFuzzy()) {
1309
- this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1310
- const els = await this._locate(matchedLocator)
1311
- assertElementExists(els, select, 'Selectable element')
1312
- return proceedSelect.call(this, els[0], option)
1310
+ if (!Array.isArray(option)) {
1311
+ option = [option]
1313
1312
  }
1314
1313
 
1315
- // Fuzzy: try combobox
1316
- this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1317
- let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1318
- if (els?.length) return proceedSelect.call(this, els[0], option)
1314
+ // select options by visible text
1315
+ let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
1319
1316
 
1320
- // Fuzzy: try listbox
1321
- els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1322
- if (els?.length) return proceedSelect.call(this, els[0], option)
1317
+ const clickOptionFn = async el => {
1318
+ if (el[0]) el = el[0]
1319
+ const elementId = getElementId(el)
1320
+ if (elementId) return this.browser.elementClick(elementId)
1321
+ }
1323
1322
 
1324
- // Fuzzy: try native select
1325
- els = await findFields.call(this, select)
1326
- assertElementExists(els, select, 'Selectable element')
1327
- return proceedSelect.call(this, els[0], option)
1323
+ if (Array.isArray(els) && els.length) {
1324
+ return forEachAsync(els, clickOptionFn)
1325
+ }
1326
+ // select options by value
1327
+ els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
1328
+ if (els.length === 0) {
1329
+ throw new ElementNotFound(select, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
1330
+ }
1331
+ return forEachAsync(els, clickOptionFn)
1328
1332
  }
1329
1333
 
1330
1334
  /**
@@ -3014,91 +3018,6 @@ async function findFields(locator) {
3014
3018
  return await this._locate(locator.value) // by css or xpath
3015
3019
  }
3016
3020
 
3017
- async function proceedSelect(el, option) {
3018
- const elementId = getElementId(el)
3019
- const role = await this.browser.getElementAttribute(elementId, 'role')
3020
- const options = Array.isArray(option) ? option : [option]
3021
-
3022
- if (role === 'combobox') {
3023
- this.debugSection('SelectOption', 'Expanding combobox')
3024
- highlightActiveElement.call(this, el)
3025
- const ariaOwns = await this.browser.getElementAttribute(elementId, 'aria-owns')
3026
- const ariaControls = await this.browser.getElementAttribute(elementId, 'aria-controls')
3027
- await this.browser.elementClick(elementId)
3028
- await this._waitForAction()
3029
-
3030
- const listboxId = ariaOwns || ariaControls
3031
- let listboxEls = listboxId ? await this.browser.$$(`#${listboxId}`) : []
3032
- if (!listboxEls.length) {
3033
- listboxEls = await this.browser.$$('[role="listbox"]')
3034
- }
3035
- if (!listboxEls.length) throw new Error('Cannot find listbox for combobox')
3036
- const listbox = listboxEls[0]
3037
- const listboxElId = getElementId(listbox)
3038
-
3039
- for (const opt of options) {
3040
- const optionEls = await this.browser.findElementsFromElement(listboxElId, 'css selector', '[role="option"]')
3041
- let optEl = null
3042
- for (const optionEl of optionEls) {
3043
- const optElId = getElementId(optionEl)
3044
- const text = await this.browser.getElementText(optElId)
3045
- if (text === opt || (text && text.includes(opt))) {
3046
- optEl = optionEl
3047
- break
3048
- }
3049
- }
3050
- if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3051
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
3052
- highlightActiveElement.call(this, optEl)
3053
- await this.browser.elementClick(getElementId(optEl))
3054
- }
3055
- return this._waitForAction()
3056
- }
3057
-
3058
- if (role === 'listbox') {
3059
- highlightActiveElement.call(this, el)
3060
- for (const opt of options) {
3061
- const optionEls = await this.browser.findElementsFromElement(elementId, 'css selector', '[role="option"]')
3062
- let optEl = null
3063
- for (const optionEl of optionEls) {
3064
- const optElId = getElementId(optionEl)
3065
- const text = await this.browser.getElementText(optElId)
3066
- if (text === opt || (text && text.includes(opt))) {
3067
- optEl = optionEl
3068
- break
3069
- }
3070
- }
3071
- if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3072
- this.debugSection('SelectOption', `Clicking: "${opt}"`)
3073
- highlightActiveElement.call(this, optEl)
3074
- await this.browser.elementClick(getElementId(optEl))
3075
- }
3076
- return this._waitForAction()
3077
- }
3078
-
3079
- // Native <select> element
3080
- highlightActiveElement.call(this, el)
3081
-
3082
- // select options by visible text
3083
- let els = await forEachAsync(options, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
3084
-
3085
- const clickOptionFn = async optEl => {
3086
- if (optEl[0]) optEl = optEl[0]
3087
- const optElId = getElementId(optEl)
3088
- if (optElId) return this.browser.elementClick(optElId)
3089
- }
3090
-
3091
- if (Array.isArray(els) && els.length) {
3092
- return forEachAsync(els, clickOptionFn)
3093
- }
3094
- // select options by value
3095
- els = await forEachAsync(options, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
3096
- if (els.length === 0) {
3097
- throw new ElementNotFound(el, `Option "${options}" in`, 'was not found neither by a visible text nor by a value')
3098
- }
3099
- return forEachAsync(els, clickOptionFn)
3100
- }
3101
-
3102
3021
  async function proceedSeeField(assertType, field, value) {
3103
3022
  const res = await findFields.call(this, field)
3104
3023
  assertElementExists(res, field, 'Field')
@@ -11,20 +11,22 @@ function buildLocatorString(locator) {
11
11
  }
12
12
 
13
13
  async function findElements(matcher, locator) {
14
- const matchedLocator = Locator.from(locator, 'css')
14
+ const matchedLocator = new Locator(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)
18
19
  if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator)
19
20
 
20
21
  return matcher.locator(buildLocatorString(matchedLocator)).all()
21
22
  }
22
23
 
23
24
  async function findElement(matcher, locator) {
24
- const matchedLocator = Locator.from(locator, 'css')
25
+ const matchedLocator = new Locator(locator, 'css')
25
26
 
26
27
  if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
27
28
  if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
29
+ if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator, { first: true })
28
30
  if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator, { first: true })
29
31
 
30
32
  return matcher.locator(buildLocatorString(matchedLocator)).first()
@@ -44,30 +46,49 @@ async function getVisibleElements(elements) {
44
46
  }
45
47
 
46
48
  async function findReact(matcher, locator) {
47
- const props = locator.locator?.props
48
- let locatorString = `_react=${locator.value}`
49
+ const details = locator.locator ?? { react: locator.value }
50
+ let locatorString = `_react=${details.react}`
49
51
 
50
- if (props) {
51
- locatorString += propBuilder(props)
52
+ if (details.props) {
53
+ locatorString += propBuilder(details.props)
52
54
  }
53
55
 
54
56
  return matcher.locator(locatorString).all()
55
57
  }
56
58
 
57
59
  async function findVue(matcher, locator) {
58
- const props = locator.locator?.props
59
- let locatorString = `_vue=${locator.value}`
60
+ const details = locator.locator ?? { vue: locator.value }
61
+ let locatorString = `_vue=${details.vue}`
60
62
 
61
- if (props) {
62
- locatorString += propBuilder(props)
63
+ if (details.props) {
64
+ locatorString += propBuilder(details.props)
63
65
  }
64
66
 
65
67
  return matcher.locator(locatorString).all()
66
68
  }
67
69
 
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
+
68
78
  async function findByRole(matcher, locator, { first = false } = {}) {
69
- const roleOptions = locator.getRoleOptions()
70
- const roleLocator = matcher.getByRole(roleOptions.role, roleOptions.options)
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)
71
92
  return first ? roleLocator.first() : roleLocator.all()
72
93
  }
73
94
 
@@ -86,4 +107,4 @@ function propBuilder(props) {
86
107
  return _props
87
108
  }
88
109
 
89
- export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByRole }
110
+ export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole }
@@ -0,0 +1,52 @@
1
+ async function findReact(matcher, locator) {
2
+ // Handle both Locator objects and raw locator objects
3
+ const reactLocator = locator.locator || locator
4
+ let _locator = `_react=${reactLocator.react}`;
5
+ let props = '';
6
+
7
+ if (reactLocator.props) {
8
+ props += propBuilder(reactLocator.props);
9
+ _locator += props;
10
+ }
11
+ return matcher.locator(_locator).all();
12
+ }
13
+
14
+ async function findVue(matcher, locator) {
15
+ // Handle both Locator objects and raw locator objects
16
+ const vueLocator = locator.locator || locator
17
+ let _locator = `_vue=${vueLocator.vue}`;
18
+ let props = '';
19
+
20
+ if (vueLocator.props) {
21
+ props += propBuilder(vueLocator.props);
22
+ _locator += props;
23
+ }
24
+ return matcher.locator(_locator).all();
25
+ }
26
+
27
+ async function findByPlaywrightLocator(matcher, locator) {
28
+ // Handle both Locator objects and raw locator objects
29
+ const pwLocator = locator.locator || locator
30
+ if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
31
+ return matcher.getByTestId(pwLocator.pw.value.split('=')[1]);
32
+ }
33
+ const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
34
+ return matcher.locator(pwValue).all();
35
+ }
36
+
37
+ function propBuilder(props) {
38
+ let _props = '';
39
+
40
+ for (const [key, value] of Object.entries(props)) {
41
+ if (typeof value === 'object') {
42
+ for (const [k, v] of Object.entries(value)) {
43
+ _props += `[${key}.${k} = "${v}"]`;
44
+ }
45
+ } else {
46
+ _props += `[${key} = "${value}"]`;
47
+ }
48
+ }
49
+ return _props;
50
+ }
51
+
52
+ export { findReact, findVue, findByPlaywrightLocator };
package/lib/locator.js CHANGED
@@ -5,10 +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', 'role']
9
-
10
- // Roles that can be used as shorthand locators: { button: 'text' } -> { role: 'button', text: 'text' }
11
- const shorthandRoles = ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox', 'heading', 'listitem', 'menuitem', 'tab', 'option', 'searchbox', 'alert', 'dialog']
8
+ const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'pw', 'role']
12
9
  /** @class */
13
10
  class Locator {
14
11
  /**
@@ -27,16 +24,19 @@ class Locator {
27
24
  */
28
25
  this.strict = false
29
26
 
30
- if (typeof locator === 'string' && this.parsedJsonAsString(locator)) {
31
- return
32
- }
33
-
34
27
  if (typeof locator === 'object') {
35
28
  if (locator.constructor.name === 'Locator') {
36
29
  Object.assign(this, locator)
37
30
  return
38
31
  }
39
- this._applyObjectLocator(locator)
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
+
40
40
  return
41
41
  }
42
42
 
@@ -53,9 +53,8 @@ class Locator {
53
53
  if (isShadow(locator)) {
54
54
  this.type = 'shadow'
55
55
  }
56
- if (isReactVueLocator(locator)) {
57
- // React/Vue locators - keep as fuzzy type, helpers will handle them specially
58
- this.type = 'fuzzy'
56
+ if (isPlaywrightLocator(locator)) {
57
+ this.type = 'pw'
59
58
  }
60
59
 
61
60
  Locator.filters.forEach(f => f(locator, this))
@@ -77,6 +76,8 @@ class Locator {
77
76
  return this.value
78
77
  case 'shadow':
79
78
  return { shadow: this.value }
79
+ case 'pw':
80
+ return { pw: this.value }
80
81
  case 'role':
81
82
  return `[role="${this.value}"]`
82
83
  }
@@ -85,62 +86,14 @@ class Locator {
85
86
 
86
87
  toStrict() {
87
88
  if (!this.type) return null
88
- if (this.type === 'role' && this.locator) {
89
- return this.locator
90
- }
91
89
  return { [this.type]: this.value }
92
90
  }
93
91
 
94
- parsedJsonAsString(locator) {
95
- if (typeof locator !== 'string') {
96
- return false
97
- }
98
-
99
- const trimmed = locator.trim()
100
- if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
101
- return false
102
- }
103
-
104
- try {
105
- const parsed = JSON.parse(trimmed)
106
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
107
- this._applyObjectLocator(parsed)
108
- return true
109
- }
110
- } catch (e) {
111
- }
112
- return false
113
- }
114
-
115
- _applyObjectLocator(locator) {
116
- this.strict = true
117
- this.locator = locator
118
- const keys = Object.keys(locator)
119
- const [type] = keys
120
-
121
- // Transform shorthand role locators: { button: 'text' } -> { role: 'button', text: 'text' }
122
- if (keys.length === 1 && shorthandRoles.includes(type)) {
123
- this.locator = { role: type, text: locator[type] }
124
- this.type = 'role'
125
- this.value = this.locator
126
- Locator.filters.forEach(f => f(this.locator, this))
127
- return
128
- }
129
-
130
- this.type = type
131
- this.value = keys.length > 1 ? locator : locator[type]
132
- Locator.filters.forEach(f => f(locator, this))
133
- }
134
-
135
92
  /**
136
93
  * @returns {string}
137
94
  */
138
95
  toString() {
139
- if (this.output) return this.output
140
- if (this.locator && this.value === this.locator) {
141
- return JSON.stringify(this.locator)
142
- }
143
- return `{${this.type}: ${this.value}}`
96
+ return this.output || `{${this.type}: ${this.value}}`
144
97
  }
145
98
 
146
99
  /**
@@ -174,27 +127,17 @@ class Locator {
174
127
  /**
175
128
  * @returns {boolean}
176
129
  */
177
- isRole() {
178
- return this.type === 'role'
130
+ isPlaywrightLocator() {
131
+ return this.type === 'pw'
179
132
  }
180
133
 
181
134
  /**
182
- * @returns {{role: string, options: object}|null}
135
+ * @returns {boolean}
183
136
  */
184
- getRoleOptions() {
185
- if (!this.isRole()) return null
186
- const data = this.locator && typeof this.locator === 'object' ? this.locator : { role: this.value }
187
- const { role, text, name, exact, includeHidden, ...rest } = data
188
- let options = { ...rest }
189
- const accessibleName = name ?? text
190
- if (accessibleName !== undefined) options.name = accessibleName
191
- if (exact !== undefined) options.exact = exact
192
- if (includeHidden !== undefined) options.includeHidden = includeHidden
193
- if (Object.keys(options).length === 0) options = undefined
194
- return { role, options }
137
+ isRole() {
138
+ return this.type === 'role'
195
139
  }
196
140
 
197
-
198
141
  /**
199
142
  * @returns {boolean}
200
143
  */
@@ -461,16 +404,6 @@ Locator.build = locator => {
461
404
  return new Locator(locator, 'css')
462
405
  }
463
406
 
464
- /**
465
- * @param {CodeceptJS.LocatorOrString|Locator} locator
466
- * @param {string} [defaultType]
467
- * @returns {Locator}
468
- */
469
- Locator.from = (locator, defaultType = '') => {
470
- if (locator instanceof Locator) return locator
471
- return new Locator(locator, defaultType)
472
- }
473
-
474
407
  /**
475
408
  * Filters to modify locators
476
409
  * @type {Array<function(CodeceptJS.LocatorOrString, Locator): void>}
@@ -513,7 +446,7 @@ Locator.clickable = {
513
446
  `.//*[@aria-label = ${literal}]`,
514
447
  `.//*[@title = ${literal}]`,
515
448
  `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
516
- `.//*[@role='button' or @role='link' or @role='tab' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='radio' or @role='checkbox' or @role='switch' or @role='treeitem' or @role='gridcell' or @role='columnheader' or @role='rowheader' or @role='scrollbar' or @role='slider' or @role='spinbutton'][normalize-space(.)=${literal}]`,
449
+ `.//*[@role='button'][normalize-space(.)=${literal}]`,
517
450
  ]),
518
451
 
519
452
  /**
@@ -671,10 +604,20 @@ function removePrefix(xpath) {
671
604
  * @param {string} locator
672
605
  * @returns {boolean}
673
606
  */
674
- function isReactVueLocator(locator) {
607
+ function isPlaywrightLocator(locator) {
675
608
  return locator.includes('_react') || locator.includes('_vue')
676
609
  }
677
610
 
611
+ /**
612
+ * @private
613
+ * check if the locator is a role locator
614
+ * @param {{role: string}} locator
615
+ * @returns {boolean}
616
+ */
617
+ function isRoleLocator(locator) {
618
+ return locator.role !== undefined && typeof locator.role === 'string' && Object.keys(locator).length >= 1
619
+ }
620
+
678
621
  /**
679
622
  * @private
680
623
  * @param {CodeceptJS.LocatorOrString} locator
package/lib/mocha/test.js CHANGED
@@ -154,14 +154,16 @@ function cloneTest(test) {
154
154
  function testToFileName(test, { suffix = '', unique = false } = {}) {
155
155
  let fileName = test.title
156
156
 
157
- if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
158
- if (suffix) fileName = `${fileName}_${suffix}`
159
157
  // remove tags with empty string (disable for now)
160
158
  // fileName = fileName.replace(/\@\w+/g, '')
161
159
  fileName = fileName.slice(0, 100)
162
160
  if (fileName.indexOf('{') !== -1) {
163
161
  fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
164
162
  }
163
+
164
+ // Apply unique suffix AFTER removing data part to ensure uniqueness
165
+ if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime())}`
166
+ if (suffix) fileName = `${fileName}_${suffix}`
165
167
  if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
166
168
  // TODO: add suite title to file name
167
169
  // if (test.parent && test.parent.title) {