codeceptjs 4.0.0-rc.8 → 4.0.0-rc.9
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/lib/element/WebElement.js +1 -1
- package/lib/helper/Playwright.js +36 -48
- package/lib/helper/Puppeteer.js +23 -22
- package/lib/helper/WebDriver.js +29 -22
- package/lib/helper/extras/elementSelection.js +51 -0
- package/lib/step/config.js +8 -2
- package/package.json +1 -1
- package/typings/index.d.ts +19 -0
package/lib/helper/Playwright.js
CHANGED
|
@@ -38,6 +38,7 @@ import Console from './extras/Console.js'
|
|
|
38
38
|
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
39
39
|
import { dropFile } from './scripts/dropFile.js'
|
|
40
40
|
import WebElement from '../element/WebElement.js'
|
|
41
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
41
42
|
|
|
42
43
|
let playwright
|
|
43
44
|
let perfTiming
|
|
@@ -1779,8 +1780,7 @@ class Playwright extends Helper {
|
|
|
1779
1780
|
if (elements.length === 0) {
|
|
1780
1781
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1781
1782
|
}
|
|
1782
|
-
|
|
1783
|
-
return elements[0]
|
|
1783
|
+
return selectElement(elements, locator, this)
|
|
1784
1784
|
}
|
|
1785
1785
|
|
|
1786
1786
|
/**
|
|
@@ -1795,8 +1795,7 @@ class Playwright extends Helper {
|
|
|
1795
1795
|
const context = providedContext || (await this._getContext())
|
|
1796
1796
|
const els = await findCheckable.call(this, locator, context)
|
|
1797
1797
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1798
|
-
|
|
1799
|
-
return els[0]
|
|
1798
|
+
return selectElement(els, locator, this)
|
|
1800
1799
|
}
|
|
1801
1800
|
|
|
1802
1801
|
/**
|
|
@@ -2282,8 +2281,7 @@ class Playwright extends Helper {
|
|
|
2282
2281
|
async fillField(field, value, context = null) {
|
|
2283
2282
|
const els = await findFields.call(this, field, context)
|
|
2284
2283
|
assertElementExists(els, field, 'Field')
|
|
2285
|
-
|
|
2286
|
-
const el = els[0]
|
|
2284
|
+
const el = selectElement(els, field, this)
|
|
2287
2285
|
|
|
2288
2286
|
await el.clear()
|
|
2289
2287
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
@@ -2301,9 +2299,8 @@ class Playwright extends Helper {
|
|
|
2301
2299
|
async clearField(locator, context = null) {
|
|
2302
2300
|
const els = await findFields.call(this, locator, context)
|
|
2303
2301
|
assertElementExists(els, locator, 'Field to clear')
|
|
2304
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
2305
2302
|
|
|
2306
|
-
const el = els
|
|
2303
|
+
const el = selectElement(els, locator, this)
|
|
2307
2304
|
|
|
2308
2305
|
await highlightActiveElement.call(this, el)
|
|
2309
2306
|
|
|
@@ -2318,10 +2315,10 @@ class Playwright extends Helper {
|
|
|
2318
2315
|
async appendField(field, value, context = null) {
|
|
2319
2316
|
const els = await findFields.call(this, field, context)
|
|
2320
2317
|
assertElementExists(els, field, 'Field')
|
|
2321
|
-
|
|
2322
|
-
await highlightActiveElement.call(this,
|
|
2323
|
-
await
|
|
2324
|
-
await
|
|
2318
|
+
const el = selectElement(els, field, this)
|
|
2319
|
+
await highlightActiveElement.call(this, el)
|
|
2320
|
+
await el.press('End')
|
|
2321
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2325
2322
|
return this._waitForAction()
|
|
2326
2323
|
}
|
|
2327
2324
|
|
|
@@ -2353,22 +2350,24 @@ class Playwright extends Helper {
|
|
|
2353
2350
|
}
|
|
2354
2351
|
const els = await findFields.call(this, locator, context)
|
|
2355
2352
|
if (els.length) {
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2353
|
+
const el = selectElement(els, locator, this)
|
|
2354
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2355
|
+
const type = await el.evaluate(el => el.type)
|
|
2358
2356
|
if (tag === 'INPUT' && type === 'file') {
|
|
2359
|
-
await
|
|
2357
|
+
await el.setInputFiles(file)
|
|
2360
2358
|
return this._waitForAction()
|
|
2361
2359
|
}
|
|
2362
2360
|
}
|
|
2363
2361
|
|
|
2364
2362
|
const targetEls = els.length ? els : await this._locate(locator)
|
|
2365
2363
|
assertElementExists(targetEls, locator, 'Element')
|
|
2364
|
+
const el = selectElement(targetEls, locator, this)
|
|
2366
2365
|
const fileData = {
|
|
2367
2366
|
base64Content: base64EncodeFile(file),
|
|
2368
2367
|
fileName: path.basename(file),
|
|
2369
2368
|
mimeType: getMimeType(path.basename(file)),
|
|
2370
2369
|
}
|
|
2371
|
-
await
|
|
2370
|
+
await el.evaluate(dropFile, fileData)
|
|
2372
2371
|
return this._waitForAction()
|
|
2373
2372
|
}
|
|
2374
2373
|
|
|
@@ -2391,23 +2390,23 @@ class Playwright extends Helper {
|
|
|
2391
2390
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2392
2391
|
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2393
2392
|
assertElementExists(els, select, 'Selectable element')
|
|
2394
|
-
return proceedSelect.call(this, pageContext, els
|
|
2393
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2395
2394
|
}
|
|
2396
2395
|
|
|
2397
2396
|
// Fuzzy: try combobox
|
|
2398
2397
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2399
2398
|
const comboboxSearchCtx = contextEl || pageContext
|
|
2400
2399
|
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2401
|
-
if (els?.length) return proceedSelect.call(this, pageContext, els
|
|
2400
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2402
2401
|
|
|
2403
2402
|
// Fuzzy: try listbox
|
|
2404
2403
|
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2405
|
-
if (els?.length) return proceedSelect.call(this, pageContext, els
|
|
2404
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2406
2405
|
|
|
2407
2406
|
// Fuzzy: try native select
|
|
2408
2407
|
els = await findFields.call(this, select, context)
|
|
2409
2408
|
assertElementExists(els, select, 'Selectable element')
|
|
2410
|
-
return proceedSelect.call(this, pageContext, els
|
|
2409
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2411
2410
|
}
|
|
2412
2411
|
|
|
2413
2412
|
/**
|
|
@@ -4282,16 +4281,21 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4282
4281
|
assertElementExists(els, locator, 'Clickable element')
|
|
4283
4282
|
}
|
|
4284
4283
|
|
|
4285
|
-
|
|
4286
|
-
|
|
4284
|
+
const elementIndex = store.currentStep?.opts?.elementIndex
|
|
4285
|
+
let element
|
|
4286
|
+
if (elementIndex != null) {
|
|
4287
|
+
element = selectElement(els, locator, this)
|
|
4288
|
+
} else {
|
|
4289
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4290
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
await highlightActiveElement.call(this, element)
|
|
4294
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4287
4295
|
|
|
4288
|
-
/*
|
|
4289
|
-
using the force true options itself but instead dispatching a click
|
|
4290
|
-
*/
|
|
4291
4296
|
if (options.force) {
|
|
4292
|
-
await
|
|
4297
|
+
await element.dispatchEvent('click')
|
|
4293
4298
|
} else {
|
|
4294
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4295
4299
|
await element.click(options)
|
|
4296
4300
|
}
|
|
4297
4301
|
const promises = []
|
|
@@ -4308,7 +4312,6 @@ async function findClickable(matcher, locator) {
|
|
|
4308
4312
|
|
|
4309
4313
|
if (!matchedLocator.isFuzzy()) {
|
|
4310
4314
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4311
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4312
4315
|
return els
|
|
4313
4316
|
}
|
|
4314
4317
|
|
|
@@ -4317,42 +4320,27 @@ async function findClickable(matcher, locator) {
|
|
|
4317
4320
|
|
|
4318
4321
|
try {
|
|
4319
4322
|
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4320
|
-
if (els.length)
|
|
4321
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4322
|
-
return els
|
|
4323
|
-
}
|
|
4323
|
+
if (els.length) return els
|
|
4324
4324
|
} catch (err) {
|
|
4325
4325
|
// getByRole not supported or failed
|
|
4326
4326
|
}
|
|
4327
4327
|
|
|
4328
4328
|
try {
|
|
4329
4329
|
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4330
|
-
if (els.length)
|
|
4331
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4332
|
-
return els
|
|
4333
|
-
}
|
|
4330
|
+
if (els.length) return els
|
|
4334
4331
|
} catch (err) {
|
|
4335
4332
|
// getByRole not supported or failed
|
|
4336
4333
|
}
|
|
4337
4334
|
|
|
4338
4335
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4339
|
-
if (els.length)
|
|
4340
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4341
|
-
return els
|
|
4342
|
-
}
|
|
4336
|
+
if (els.length) return els
|
|
4343
4337
|
|
|
4344
4338
|
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
4345
|
-
if (els.length)
|
|
4346
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4347
|
-
return els
|
|
4348
|
-
}
|
|
4339
|
+
if (els.length) return els
|
|
4349
4340
|
|
|
4350
4341
|
try {
|
|
4351
4342
|
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
4352
|
-
if (els.length)
|
|
4353
|
-
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4354
|
-
return els
|
|
4355
|
-
}
|
|
4343
|
+
if (els.length) return els
|
|
4356
4344
|
} catch (err) {
|
|
4357
4345
|
// Do nothing
|
|
4358
4346
|
}
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -43,6 +43,7 @@ import { dropFile } from './scripts/dropFile.js'
|
|
|
43
43
|
import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
|
|
44
44
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
45
45
|
import WebElement from '../element/WebElement.js'
|
|
46
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
46
47
|
|
|
47
48
|
let puppeteer
|
|
48
49
|
|
|
@@ -1008,13 +1009,13 @@ class Puppeteer extends Helper {
|
|
|
1008
1009
|
*/
|
|
1009
1010
|
async _locateElement(locator) {
|
|
1010
1011
|
const context = await this.context
|
|
1011
|
-
|
|
1012
|
+
const elementIndex = store.currentStep?.opts?.elementIndex
|
|
1013
|
+
if (this.options.strict || elementIndex) {
|
|
1012
1014
|
const elements = await findElements.call(this, context, locator)
|
|
1013
1015
|
if (elements.length === 0) {
|
|
1014
1016
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1015
1017
|
}
|
|
1016
|
-
|
|
1017
|
-
return elements[0]
|
|
1018
|
+
return selectElement(elements, locator, this)
|
|
1018
1019
|
}
|
|
1019
1020
|
return findElement.call(this, context, locator)
|
|
1020
1021
|
}
|
|
@@ -1033,8 +1034,7 @@ class Puppeteer extends Helper {
|
|
|
1033
1034
|
if (!els || els.length === 0) {
|
|
1034
1035
|
throw new ElementNotFound(locator, 'Checkbox or radio')
|
|
1035
1036
|
}
|
|
1036
|
-
|
|
1037
|
-
return els[0]
|
|
1037
|
+
return selectElement(els, locator, this)
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
1040
|
/**
|
|
@@ -1593,8 +1593,7 @@ class Puppeteer extends Helper {
|
|
|
1593
1593
|
async fillField(field, value, context = null) {
|
|
1594
1594
|
const els = await findVisibleFields.call(this, field, context)
|
|
1595
1595
|
assertElementExists(els, field, 'Field')
|
|
1596
|
-
|
|
1597
|
-
const el = els[0]
|
|
1596
|
+
const el = selectElement(els, field, this)
|
|
1598
1597
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1599
1598
|
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1600
1599
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
@@ -1624,10 +1623,10 @@ class Puppeteer extends Helper {
|
|
|
1624
1623
|
async appendField(field, value, context = null) {
|
|
1625
1624
|
const els = await findVisibleFields.call(this, field, context)
|
|
1626
1625
|
assertElementExists(els, field, 'Field')
|
|
1627
|
-
|
|
1628
|
-
highlightActiveElement.call(this,
|
|
1629
|
-
await
|
|
1630
|
-
await
|
|
1626
|
+
const el = selectElement(els, field, this)
|
|
1627
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
1628
|
+
await el.press('End')
|
|
1629
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
1631
1630
|
return this._waitForAction()
|
|
1632
1631
|
}
|
|
1633
1632
|
|
|
@@ -1660,22 +1659,24 @@ class Puppeteer extends Helper {
|
|
|
1660
1659
|
}
|
|
1661
1660
|
const els = await findFields.call(this, locator, context)
|
|
1662
1661
|
if (els.length) {
|
|
1663
|
-
const
|
|
1664
|
-
const
|
|
1662
|
+
const el = selectElement(els, locator, this)
|
|
1663
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
1664
|
+
const type = await el.evaluate(el => el.type)
|
|
1665
1665
|
if (tag === 'INPUT' && type === 'file') {
|
|
1666
|
-
await
|
|
1666
|
+
await el.uploadFile(file)
|
|
1667
1667
|
return this._waitForAction()
|
|
1668
1668
|
}
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
1671
|
const targetEls = els.length ? els : await this._locate(locator)
|
|
1672
1672
|
assertElementExists(targetEls, locator, 'Element')
|
|
1673
|
+
const el = selectElement(targetEls, locator, this)
|
|
1673
1674
|
const fileData = {
|
|
1674
1675
|
base64Content: base64EncodeFile(file),
|
|
1675
1676
|
fileName: path.basename(file),
|
|
1676
1677
|
mimeType: getMimeType(path.basename(file)),
|
|
1677
1678
|
}
|
|
1678
|
-
await
|
|
1679
|
+
await el.evaluate(dropFile, fileData)
|
|
1679
1680
|
return this._waitForAction()
|
|
1680
1681
|
}
|
|
1681
1682
|
|
|
@@ -1698,23 +1699,23 @@ class Puppeteer extends Helper {
|
|
|
1698
1699
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1699
1700
|
const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
|
|
1700
1701
|
assertElementExists(els, select, 'Selectable element')
|
|
1701
|
-
return proceedSelect.call(this, pageContext, els
|
|
1702
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1702
1703
|
}
|
|
1703
1704
|
|
|
1704
1705
|
// Fuzzy: try combobox
|
|
1705
1706
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1706
1707
|
const comboboxSearchCtx = contextEl || pageContext
|
|
1707
1708
|
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
1708
|
-
if (els?.length) return proceedSelect.call(this, pageContext, els
|
|
1709
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1709
1710
|
|
|
1710
1711
|
// Fuzzy: try listbox
|
|
1711
1712
|
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
1712
|
-
if (els?.length) return proceedSelect.call(this, pageContext, els
|
|
1713
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1713
1714
|
|
|
1714
1715
|
// Fuzzy: try native select
|
|
1715
1716
|
const visibleEls = await findVisibleFields.call(this, select, context)
|
|
1716
1717
|
assertElementExists(visibleEls, select, 'Selectable field')
|
|
1717
|
-
return proceedSelect.call(this, pageContext, visibleEls
|
|
1718
|
+
return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
|
|
1718
1719
|
}
|
|
1719
1720
|
|
|
1720
1721
|
/**
|
|
@@ -3121,11 +3122,11 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3121
3122
|
} else {
|
|
3122
3123
|
assertElementExists(els, locator, 'Clickable element')
|
|
3123
3124
|
}
|
|
3124
|
-
|
|
3125
|
+
const el = selectElement(els, locator, this)
|
|
3125
3126
|
|
|
3126
|
-
highlightActiveElement.call(this,
|
|
3127
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
3127
3128
|
|
|
3128
|
-
await
|
|
3129
|
+
await el.click(options)
|
|
3129
3130
|
const promises = []
|
|
3130
3131
|
if (options.waitForNavigation) {
|
|
3131
3132
|
promises.push(this.waitForNavigation())
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -40,6 +40,7 @@ import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElem
|
|
|
40
40
|
import { dropFile } from './scripts/dropFile.js'
|
|
41
41
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
42
42
|
import WebElement from '../element/WebElement.js'
|
|
43
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
43
44
|
|
|
44
45
|
const SHADOW = 'shadow'
|
|
45
46
|
const webRoot = 'body'
|
|
@@ -1093,8 +1094,7 @@ class WebDriver extends Helper {
|
|
|
1093
1094
|
} else {
|
|
1094
1095
|
assertElementExists(res, locator, 'Clickable element')
|
|
1095
1096
|
}
|
|
1096
|
-
|
|
1097
|
-
const elem = usingFirstElement(res)
|
|
1097
|
+
const elem = selectElement(res, locator, this)
|
|
1098
1098
|
highlightActiveElement.call(this, elem)
|
|
1099
1099
|
return this.browser[clickMethod](getElementId(elem))
|
|
1100
1100
|
}
|
|
@@ -1113,8 +1113,7 @@ class WebDriver extends Helper {
|
|
|
1113
1113
|
} else {
|
|
1114
1114
|
assertElementExists(res, locator, 'Clickable element')
|
|
1115
1115
|
}
|
|
1116
|
-
|
|
1117
|
-
const elem = usingFirstElement(res)
|
|
1116
|
+
const elem = selectElement(res, locator, this)
|
|
1118
1117
|
highlightActiveElement.call(this, elem)
|
|
1119
1118
|
|
|
1120
1119
|
return this.executeScript(el => {
|
|
@@ -1141,9 +1140,8 @@ class WebDriver extends Helper {
|
|
|
1141
1140
|
} else {
|
|
1142
1141
|
assertElementExists(res, locator, 'Clickable element')
|
|
1143
1142
|
}
|
|
1144
|
-
if (this.options.strict) assertOnlyOneElement(res, locator, this)
|
|
1145
1143
|
|
|
1146
|
-
const elem =
|
|
1144
|
+
const elem = selectElement(res, locator, this)
|
|
1147
1145
|
highlightActiveElement.call(this, elem)
|
|
1148
1146
|
return elem.doubleClick()
|
|
1149
1147
|
}
|
|
@@ -1162,9 +1160,8 @@ class WebDriver extends Helper {
|
|
|
1162
1160
|
} else {
|
|
1163
1161
|
assertElementExists(res, locator, 'Clickable element')
|
|
1164
1162
|
}
|
|
1165
|
-
if (this.options.strict) assertOnlyOneElement(res, locator, this)
|
|
1166
1163
|
|
|
1167
|
-
const el =
|
|
1164
|
+
const el = selectElement(res, locator, this)
|
|
1168
1165
|
|
|
1169
1166
|
await el.moveTo()
|
|
1170
1167
|
|
|
@@ -1279,8 +1276,7 @@ class WebDriver extends Helper {
|
|
|
1279
1276
|
async fillField(field, value, context = null) {
|
|
1280
1277
|
const res = await findFields.call(this, field, context)
|
|
1281
1278
|
assertElementExists(res, field, 'Field')
|
|
1282
|
-
|
|
1283
|
-
const elem = usingFirstElement(res)
|
|
1279
|
+
const elem = selectElement(res, field, this)
|
|
1284
1280
|
highlightActiveElement.call(this, elem)
|
|
1285
1281
|
try {
|
|
1286
1282
|
await elem.clearValue()
|
|
@@ -1303,8 +1299,7 @@ class WebDriver extends Helper {
|
|
|
1303
1299
|
async appendField(field, value, context = null) {
|
|
1304
1300
|
const res = await findFields.call(this, field, context)
|
|
1305
1301
|
assertElementExists(res, field, 'Field')
|
|
1306
|
-
|
|
1307
|
-
const elem = usingFirstElement(res)
|
|
1302
|
+
const elem = selectElement(res, field, this)
|
|
1308
1303
|
highlightActiveElement.call(this, elem)
|
|
1309
1304
|
return elem.addValue(value.toString())
|
|
1310
1305
|
}
|
|
@@ -1316,8 +1311,7 @@ class WebDriver extends Helper {
|
|
|
1316
1311
|
async clearField(field, context = null) {
|
|
1317
1312
|
const res = await findFields.call(this, field, context)
|
|
1318
1313
|
assertElementExists(res, field, 'Field')
|
|
1319
|
-
|
|
1320
|
-
const elem = usingFirstElement(res)
|
|
1314
|
+
const elem = selectElement(res, field, this)
|
|
1321
1315
|
highlightActiveElement.call(this, elem)
|
|
1322
1316
|
return elem.clearValue(getElementId(elem))
|
|
1323
1317
|
}
|
|
@@ -1334,22 +1328,22 @@ class WebDriver extends Helper {
|
|
|
1334
1328
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1335
1329
|
const els = await locateFn(select)
|
|
1336
1330
|
assertElementExists(els, select, 'Selectable element')
|
|
1337
|
-
return proceedSelectOption.call(this,
|
|
1331
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1338
1332
|
}
|
|
1339
1333
|
|
|
1340
1334
|
// Fuzzy: try combobox
|
|
1341
1335
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1342
1336
|
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1343
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1337
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1344
1338
|
|
|
1345
1339
|
// Fuzzy: try listbox
|
|
1346
1340
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1347
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1341
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1348
1342
|
|
|
1349
1343
|
// Fuzzy: try native select
|
|
1350
1344
|
const res = await findFields.call(this, select, context)
|
|
1351
1345
|
assertElementExists(res, select, 'Selectable field')
|
|
1352
|
-
return proceedSelectOption.call(this,
|
|
1346
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1353
1347
|
}
|
|
1354
1348
|
|
|
1355
1349
|
/**
|
|
@@ -1367,7 +1361,7 @@ class WebDriver extends Helper {
|
|
|
1367
1361
|
this.debug(`Uploading ${file}`)
|
|
1368
1362
|
|
|
1369
1363
|
if (res.length) {
|
|
1370
|
-
const el =
|
|
1364
|
+
const el = selectElement(res, locator, this)
|
|
1371
1365
|
const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
|
|
1372
1366
|
const type = await this.browser.execute(function (elem) { return elem.type }, el)
|
|
1373
1367
|
if (tag === 'INPUT' && type === 'file') {
|
|
@@ -1385,7 +1379,7 @@ class WebDriver extends Helper {
|
|
|
1385
1379
|
|
|
1386
1380
|
const targetRes = res.length ? res : await this._locate(locator)
|
|
1387
1381
|
assertElementExists(targetRes, locator, 'Element')
|
|
1388
|
-
const targetEl =
|
|
1382
|
+
const targetEl = selectElement(targetRes, locator, this)
|
|
1389
1383
|
const fileData = {
|
|
1390
1384
|
base64Content: base64EncodeFile(file),
|
|
1391
1385
|
fileName: path.basename(file),
|
|
@@ -1405,7 +1399,7 @@ class WebDriver extends Helper {
|
|
|
1405
1399
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1406
1400
|
|
|
1407
1401
|
assertElementExists(res, field, 'Checkable')
|
|
1408
|
-
const elem =
|
|
1402
|
+
const elem = selectElement(res, field, this)
|
|
1409
1403
|
const elementId = getElementId(elem)
|
|
1410
1404
|
highlightActiveElement.call(this, elem)
|
|
1411
1405
|
|
|
@@ -1426,7 +1420,7 @@ class WebDriver extends Helper {
|
|
|
1426
1420
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1427
1421
|
|
|
1428
1422
|
assertElementExists(res, field, 'Checkable')
|
|
1429
|
-
const elem =
|
|
1423
|
+
const elem = selectElement(res, field, this)
|
|
1430
1424
|
const elementId = getElementId(elem)
|
|
1431
1425
|
highlightActiveElement.call(this, elem)
|
|
1432
1426
|
|
|
@@ -3301,6 +3295,19 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3301
3295
|
}
|
|
3302
3296
|
|
|
3303
3297
|
function usingFirstElement(els) {
|
|
3298
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
3299
|
+
if (rawIndex != null && els.length > 1) {
|
|
3300
|
+
let elementIndex = rawIndex
|
|
3301
|
+
if (elementIndex === 'first') elementIndex = 1
|
|
3302
|
+
if (elementIndex === 'last') elementIndex = -1
|
|
3303
|
+
if (Number.isInteger(elementIndex) && elementIndex !== 0) {
|
|
3304
|
+
const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
|
|
3305
|
+
if (idx >= 0 && idx < els.length) {
|
|
3306
|
+
debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
|
|
3307
|
+
return els[idx]
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3304
3311
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3305
3312
|
return els[0]
|
|
3306
3313
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import store from '../../store.js'
|
|
2
|
+
import output from '../../output.js'
|
|
3
|
+
import WebElement from '../../element/WebElement.js'
|
|
4
|
+
import MultipleElementsFound from '../errors/MultipleElementsFound.js'
|
|
5
|
+
|
|
6
|
+
function resolveElementIndex(value) {
|
|
7
|
+
if (value === 'first') return 1
|
|
8
|
+
if (value === 'last') return -1
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function selectElement(els, locator, helper) {
|
|
13
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
14
|
+
const elementIndex = resolveElementIndex(rawIndex)
|
|
15
|
+
|
|
16
|
+
if (elementIndex != null) {
|
|
17
|
+
if (els.length === 1) return els[0]
|
|
18
|
+
|
|
19
|
+
if (!Number.isInteger(elementIndex) || elementIndex === 0) {
|
|
20
|
+
throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let idx
|
|
24
|
+
if (elementIndex > 0) {
|
|
25
|
+
idx = elementIndex - 1
|
|
26
|
+
if (idx >= els.length) {
|
|
27
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
idx = els.length + elementIndex
|
|
31
|
+
if (idx < 0) {
|
|
32
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
|
|
37
|
+
return els[idx]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (helper.options.strict) {
|
|
41
|
+
if (els.length > 1) {
|
|
42
|
+
const webElements = els.map(el => new WebElement(el, helper))
|
|
43
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
|
|
48
|
+
return els[0]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { selectElement }
|
package/lib/step/config.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} StepOptions
|
|
3
|
+
* @property {number|'first'|'last'} [elementIndex] - Select a specific element when multiple match. 1-based positive index, negative from end, or 'first'/'last'.
|
|
4
|
+
* @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* StepConfig is a configuration object for a step.
|
|
3
9
|
* It is used to create a new step that is a combination of other steps.
|
|
4
10
|
*/
|
|
5
11
|
class StepConfig {
|
|
6
12
|
constructor(opts = {}) {
|
|
7
|
-
/** @member {{ opts:
|
|
13
|
+
/** @member {{ opts: StepOptions, timeout: number|undefined, retry: number|undefined }} */
|
|
8
14
|
this.config = {
|
|
9
15
|
opts,
|
|
10
16
|
timeout: undefined,
|
|
@@ -14,7 +20,7 @@ class StepConfig {
|
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Set the options for the step.
|
|
17
|
-
* @param {
|
|
23
|
+
* @param {StepOptions} opts - The options for the step.
|
|
18
24
|
* @returns {StepConfig} - The step configuration object.
|
|
19
25
|
*/
|
|
20
26
|
opts(opts) {
|
package/package.json
CHANGED
package/typings/index.d.ts
CHANGED
|
@@ -745,3 +745,22 @@ declare module 'codeceptjs/effects' {
|
|
|
745
745
|
export const retryTo: RetryTo
|
|
746
746
|
export const hopeThat: HopeThat
|
|
747
747
|
}
|
|
748
|
+
|
|
749
|
+
declare module 'codeceptjs/steps' {
|
|
750
|
+
const step: {
|
|
751
|
+
opts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
|
|
752
|
+
timeout(timeout: number): CodeceptJS.StepConfig;
|
|
753
|
+
retry(retry: number): CodeceptJS.StepConfig;
|
|
754
|
+
stepOpts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
|
|
755
|
+
stepTimeout(timeout: number): CodeceptJS.StepConfig;
|
|
756
|
+
stepRetry(retry: number): CodeceptJS.StepConfig;
|
|
757
|
+
section(name: string): any;
|
|
758
|
+
endSection(): any;
|
|
759
|
+
Section(name: string): any;
|
|
760
|
+
EndSection(): any;
|
|
761
|
+
Given(): any;
|
|
762
|
+
When(): any;
|
|
763
|
+
Then(): any;
|
|
764
|
+
}
|
|
765
|
+
export default step
|
|
766
|
+
}
|