codeceptjs 4.0.1-beta.21 → 4.0.1-beta.23
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 +2 -1
- package/lib/command/{interactive.js → shell.js} +31 -3
- package/lib/helper/Playwright.js +64 -48
- package/lib/helper/Puppeteer.js +106 -25
- package/lib/helper/WebDriver.js +103 -22
- package/lib/locator.js +14 -1
- package/package.json +1 -1
package/bin/codecept.js
CHANGED
|
@@ -91,7 +91,8 @@ program
|
|
|
91
91
|
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
92
92
|
.option(commandFlags.ai.flag, commandFlags.ai.description)
|
|
93
93
|
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
94
|
-
.
|
|
94
|
+
.option('--file [path]', 'JavaScript file to execute in shell context')
|
|
95
|
+
.action(commandHandler('../lib/command/shell.js'))
|
|
95
96
|
|
|
96
97
|
program.command('list [path]').alias('l').description('List all actions for I.').action(commandHandler('../lib/command/list.js'))
|
|
97
98
|
|
|
@@ -5,9 +5,17 @@ import Container from '../container.js'
|
|
|
5
5
|
import event from '../event.js'
|
|
6
6
|
import pause from '../pause.js'
|
|
7
7
|
import output from '../output.js'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { createRequire } from 'module'
|
|
10
|
+
import path from 'path'
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = path.dirname(__filename)
|
|
15
|
+
|
|
8
16
|
const webHelpers = Container.STANDARD_ACTING_HELPERS
|
|
9
17
|
|
|
10
|
-
export default async function (
|
|
18
|
+
export default async function (shellPath, options) {
|
|
11
19
|
// Backward compatibility for --profile
|
|
12
20
|
process.profile = options.profile
|
|
13
21
|
process.env.profile = options.profile
|
|
@@ -17,7 +25,7 @@ export default async function (path, options) {
|
|
|
17
25
|
const testsPath = getTestRoot(configFile)
|
|
18
26
|
|
|
19
27
|
const codecept = new Codecept(config, options)
|
|
20
|
-
codecept.init(testsPath)
|
|
28
|
+
await codecept.init(testsPath)
|
|
21
29
|
|
|
22
30
|
try {
|
|
23
31
|
await codecept.bootstrap()
|
|
@@ -53,7 +61,27 @@ export default async function (path, options) {
|
|
|
53
61
|
break
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
|
-
|
|
64
|
+
|
|
65
|
+
if (options.file) {
|
|
66
|
+
const scriptPath = path.resolve(options.file)
|
|
67
|
+
output.print(`Executing script: ${scriptPath}`)
|
|
68
|
+
|
|
69
|
+
// Use the same I actor that pause() uses
|
|
70
|
+
const I = Container.support('I')
|
|
71
|
+
global.I = I
|
|
72
|
+
globalThis.I = I
|
|
73
|
+
|
|
74
|
+
recorder.add('execute script', async () => {
|
|
75
|
+
try {
|
|
76
|
+
await import(scriptPath)
|
|
77
|
+
output.print('Script executed successfully')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
output.error(`Error executing script: ${err.message}`)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
pause()
|
|
84
|
+
}
|
|
57
85
|
recorder.add(() => event.emit(event.test.after, {}))
|
|
58
86
|
recorder.add(() => event.emit(event.suite.after, {}))
|
|
59
87
|
recorder.add(() => event.emit(event.all.result, {}))
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -2489,35 +2489,30 @@ class Playwright extends Helper {
|
|
|
2489
2489
|
* {{> selectOption }}
|
|
2490
2490
|
*/
|
|
2491
2491
|
async selectOption(select, option) {
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2492
|
+
const context = await this.context
|
|
2493
|
+
const matchedLocator = new Locator(select)
|
|
2494
2494
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
if (!els || els.length === 0) {
|
|
2503
|
-
els = await findFields.call(this, select)
|
|
2495
|
+
// Strict locator
|
|
2496
|
+
if (!matchedLocator.isFuzzy()) {
|
|
2497
|
+
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2498
|
+
const els = await this._locate(matchedLocator)
|
|
2499
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2500
|
+
return proceedSelect.call(this, context, els[0], option)
|
|
2504
2501
|
}
|
|
2505
|
-
assertElementExists(els, select, 'Selectable field')
|
|
2506
|
-
const el = els[0]
|
|
2507
|
-
|
|
2508
|
-
await highlightActiveElement.call(this, el)
|
|
2509
|
-
let optionToSelect = ''
|
|
2510
2502
|
|
|
2511
|
-
try
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
}
|
|
2503
|
+
// Fuzzy: try combobox
|
|
2504
|
+
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2505
|
+
let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
|
|
2506
|
+
if (els?.length) return proceedSelect.call(this, context, els[0], option)
|
|
2516
2507
|
|
|
2517
|
-
|
|
2508
|
+
// Fuzzy: try listbox
|
|
2509
|
+
els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
|
|
2510
|
+
if (els?.length) return proceedSelect.call(this, context, els[0], option)
|
|
2518
2511
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2512
|
+
// Fuzzy: try native select
|
|
2513
|
+
els = await findFields.call(this, select)
|
|
2514
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2515
|
+
return proceedSelect.call(this, context, els[0], option)
|
|
2521
2516
|
}
|
|
2522
2517
|
|
|
2523
2518
|
/**
|
|
@@ -4510,32 +4505,14 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4510
4505
|
}
|
|
4511
4506
|
|
|
4512
4507
|
async function findClickable(matcher, locator) {
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
// Handle role locators from Locator
|
|
4517
|
-
if (matchedLocator.isRole()) {
|
|
4518
|
-
return findByRole(matcher, matchedLocator)
|
|
4519
|
-
}
|
|
4508
|
+
if (locator.react) return findReact(matcher, locator)
|
|
4509
|
+
if (locator.vue) return findVue(matcher, locator)
|
|
4520
4510
|
|
|
4521
|
-
|
|
4511
|
+
locator = new Locator(locator)
|
|
4512
|
+
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
|
|
4522
4513
|
|
|
4523
4514
|
let els
|
|
4524
|
-
const literal = xpathLocator.literal(
|
|
4525
|
-
|
|
4526
|
-
try {
|
|
4527
|
-
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4528
|
-
if (els.length) return els
|
|
4529
|
-
} catch (err) {
|
|
4530
|
-
// getByRole not supported or failed
|
|
4531
|
-
}
|
|
4532
|
-
|
|
4533
|
-
try {
|
|
4534
|
-
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4535
|
-
if (els.length) return els
|
|
4536
|
-
} catch (err) {
|
|
4537
|
-
// getByRole not supported or failed
|
|
4538
|
-
}
|
|
4515
|
+
const literal = xpathLocator.literal(locator.value)
|
|
4539
4516
|
|
|
4540
4517
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4541
4518
|
if (els.length) return els
|
|
@@ -4550,7 +4527,7 @@ async function findClickable(matcher, locator) {
|
|
|
4550
4527
|
// Do nothing
|
|
4551
4528
|
}
|
|
4552
4529
|
|
|
4553
|
-
return findElements.call(this, matcher,
|
|
4530
|
+
return findElements.call(this, matcher, locator.value) // by css or xpath
|
|
4554
4531
|
}
|
|
4555
4532
|
|
|
4556
4533
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -4649,6 +4626,45 @@ async function findFields(locator) {
|
|
|
4649
4626
|
return this._locate({ css: locator })
|
|
4650
4627
|
}
|
|
4651
4628
|
|
|
4629
|
+
async function proceedSelect(context, el, option) {
|
|
4630
|
+
const role = await el.getAttribute('role')
|
|
4631
|
+
const options = Array.isArray(option) ? option : [option]
|
|
4632
|
+
|
|
4633
|
+
if (role === 'combobox') {
|
|
4634
|
+
this.debugSection('SelectOption', 'Expanding combobox')
|
|
4635
|
+
await highlightActiveElement.call(this, el)
|
|
4636
|
+
const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
|
|
4637
|
+
await el.click()
|
|
4638
|
+
await this._waitForAction()
|
|
4639
|
+
|
|
4640
|
+
const listboxId = ariaOwns || ariaControls
|
|
4641
|
+
let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
|
|
4642
|
+
if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
|
|
4643
|
+
|
|
4644
|
+
for (const opt of options) {
|
|
4645
|
+
const optEl = listbox.getByRole('option', { name: opt }).first()
|
|
4646
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4647
|
+
await highlightActiveElement.call(this, optEl)
|
|
4648
|
+
await optEl.click()
|
|
4649
|
+
}
|
|
4650
|
+
return this._waitForAction()
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
if (role === 'listbox') {
|
|
4654
|
+
for (const opt of options) {
|
|
4655
|
+
const optEl = el.getByRole('option', { name: opt }).first()
|
|
4656
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4657
|
+
await highlightActiveElement.call(this, optEl)
|
|
4658
|
+
await optEl.click()
|
|
4659
|
+
}
|
|
4660
|
+
return this._waitForAction()
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
await highlightActiveElement.call(this, el)
|
|
4664
|
+
await el.selectOption(option)
|
|
4665
|
+
return this._waitForAction()
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4652
4668
|
async function proceedSeeInField(assertType, field, value) {
|
|
4653
4669
|
const els = await findFields.call(this, field)
|
|
4654
4670
|
assertElementExists(els, field, 'Field')
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -1617,33 +1617,30 @@ class Puppeteer extends Helper {
|
|
|
1617
1617
|
* {{> selectOption }}
|
|
1618
1618
|
*/
|
|
1619
1619
|
async selectOption(select, option) {
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
-
}
|
|
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)
|
|
1640
1629
|
}
|
|
1641
|
-
await this._evaluateHandeInContext(element => {
|
|
1642
|
-
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1643
|
-
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1644
|
-
}, el)
|
|
1645
1630
|
|
|
1646
|
-
|
|
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)
|
|
1647
1644
|
}
|
|
1648
1645
|
|
|
1649
1646
|
/**
|
|
@@ -3160,6 +3157,90 @@ async function findFields(locator) {
|
|
|
3160
3157
|
return this._locate({ css: matchedLocator.value })
|
|
3161
3158
|
}
|
|
3162
3159
|
|
|
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
|
+
|
|
3163
3244
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
3164
3245
|
const src = await this._locateElement(sourceLocator)
|
|
3165
3246
|
if (!src) {
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1302,33 +1302,29 @@ class WebDriver extends Helper {
|
|
|
1302
1302
|
* {{> selectOption }}
|
|
1303
1303
|
*/
|
|
1304
1304
|
async selectOption(select, option) {
|
|
1305
|
-
const
|
|
1306
|
-
assertElementExists(res, select, 'Selectable field')
|
|
1307
|
-
const elem = usingFirstElement(res)
|
|
1308
|
-
highlightActiveElement.call(this, elem)
|
|
1305
|
+
const matchedLocator = new Locator(select)
|
|
1309
1306
|
|
|
1310
|
-
|
|
1311
|
-
|
|
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)
|
|
1312
1313
|
}
|
|
1313
1314
|
|
|
1314
|
-
//
|
|
1315
|
-
|
|
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)
|
|
1316
1319
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (elementId) return this.browser.elementClick(elementId)
|
|
1321
|
-
}
|
|
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)
|
|
1322
1323
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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)
|
|
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)
|
|
1332
1328
|
}
|
|
1333
1329
|
|
|
1334
1330
|
/**
|
|
@@ -3018,6 +3014,91 @@ async function findFields(locator) {
|
|
|
3018
3014
|
return await this._locate(locator.value) // by css or xpath
|
|
3019
3015
|
}
|
|
3020
3016
|
|
|
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
|
+
|
|
3021
3102
|
async function proceedSeeField(assertType, field, value) {
|
|
3022
3103
|
const res = await findFields.call(this, field)
|
|
3023
3104
|
assertElementExists(res, field, 'Field')
|
package/lib/locator.js
CHANGED
|
@@ -6,6 +6,9 @@ const require = createRequire(import.meta.url)
|
|
|
6
6
|
let cssToXPath
|
|
7
7
|
|
|
8
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']
|
|
9
12
|
/** @class */
|
|
10
13
|
class Locator {
|
|
11
14
|
/**
|
|
@@ -114,6 +117,16 @@ class Locator {
|
|
|
114
117
|
this.locator = locator
|
|
115
118
|
const keys = Object.keys(locator)
|
|
116
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
|
+
|
|
117
130
|
this.type = type
|
|
118
131
|
this.value = keys.length > 1 ? locator : locator[type]
|
|
119
132
|
Locator.filters.forEach(f => f(locator, this))
|
|
@@ -500,7 +513,7 @@ Locator.clickable = {
|
|
|
500
513
|
`.//*[@aria-label = ${literal}]`,
|
|
501
514
|
`.//*[@title = ${literal}]`,
|
|
502
515
|
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
|
|
503
|
-
`.//*[@role='button'][normalize-space(.)=${literal}]`,
|
|
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}]`,
|
|
504
517
|
]),
|
|
505
518
|
|
|
506
519
|
/**
|