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.
- package/bin/codecept.js +1 -2
- package/lib/command/{shell.js → interactive.js} +3 -31
- package/lib/config.js +3 -2
- package/lib/container.js +17 -3
- package/lib/helper/Playwright.js +176 -232
- package/lib/helper/Puppeteer.js +33 -111
- package/lib/helper/WebDriver.js +22 -103
- package/lib/helper/extras/PlaywrightLocator.js +34 -13
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/locator.js +31 -88
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/utils/typescript.js +61 -34
- package/package.json +8 -8
- package/typings/index.d.ts +1 -1
- package/typings/promiseBasedTypes.d.ts +5475 -3929
- package/typings/types.d.ts +5767 -4092
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -1617,30 +1617,33 @@ class Puppeteer extends Helper {
|
|
|
1617
1617
|
* {{> selectOption }}
|
|
1618
1618
|
*/
|
|
1619
1619
|
async selectOption(select, option) {
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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) {
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1302,29 +1302,33 @@ class WebDriver extends Helper {
|
|
|
1302
1302
|
* {{> selectOption }}
|
|
1303
1303
|
*/
|
|
1304
1304
|
async selectOption(select, option) {
|
|
1305
|
-
const
|
|
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
|
-
|
|
1308
|
-
|
|
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
|
-
//
|
|
1316
|
-
this.
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
|
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
|
|
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
|
|
48
|
-
let locatorString = `_react=${
|
|
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
|
|
59
|
-
let locatorString = `_vue=${
|
|
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
|
|
70
|
-
const
|
|
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
|
-
|
|
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 (
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
return this.type === '
|
|
130
|
+
isPlaywrightLocator() {
|
|
131
|
+
return this.type === 'pw'
|
|
179
132
|
}
|
|
180
133
|
|
|
181
134
|
/**
|
|
182
|
-
* @returns {
|
|
135
|
+
* @returns {boolean}
|
|
183
136
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
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'
|
|
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
|
|
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) {
|