codeceptjs 4.1.0 → 4.2.0-beta.1
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/docs/alternative-browsers.md +153 -0
- package/docs/basics.md +9 -1
- package/docs/configuration.md +2 -0
- package/docs/helpers/CDPBrowser.md +2138 -0
- package/docs/helpers/Kitesurf.md +118 -0
- package/docs/helpers/Obscura.md +210 -0
- package/docs/migration-4.md +3 -1
- package/docs/parallel.md +10 -0
- package/docs/plugins/screencast.md +18 -13
- package/docs/plugins.md +1 -1
- package/lib/command/info.js +11 -3
- package/lib/command/workers/runTests.js +14 -20
- package/lib/container.js +6 -0
- package/lib/data/context.js +4 -0
- package/lib/element/WebElement.js +5 -0
- package/lib/helper/Appium.js +14 -2
- package/lib/helper/CDPBrowser.js +3004 -0
- package/lib/helper/Kitesurf.js +139 -0
- package/lib/helper/Obscura.js +344 -0
- package/lib/helper/Playwright.js +30 -3
- package/lib/helper/Puppeteer.js +43 -16
- package/lib/helper/WebDriver.js +43 -8
- package/lib/helper/clientscripts/cdpBrowserClient.js +486 -0
- package/lib/helper/clientscripts/xpathPolyfill.js +31 -0
- package/lib/helper/extras/CDPConnection.js +92 -0
- package/lib/helper/extras/CDPElementHandle.js +27 -0
- package/lib/helper/extras/apngAssembler.js +156 -0
- package/lib/html.js +9 -2
- package/lib/listener/retryEnhancer.js +2 -1
- package/lib/listener/steps.js +8 -0
- package/lib/mocha/hooks.js +10 -0
- package/lib/parser.js +14 -2
- package/lib/plugin/junitReporter.js +17 -1
- package/lib/plugin/screencast.js +116 -24
- package/lib/step/base.js +15 -3
- package/lib/utils/loaderCheck.js +6 -0
- package/lib/utils.js +1 -1
- package/lib/workers.js +17 -0
- package/package.json +4 -1
- package/typings/promiseBasedTypes.d.ts +1833 -0
- package/typings/types.d.ts +1840 -0
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -64,6 +64,7 @@ function wrapError(e) {
|
|
|
64
64
|
let perfTiming
|
|
65
65
|
const popupStore = new Popup()
|
|
66
66
|
const consoleLogStore = new Console()
|
|
67
|
+
const checkableRoles = ['checkbox', 'radio', 'switch']
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* ## Configuration
|
|
@@ -560,7 +561,7 @@ class Puppeteer extends Helper {
|
|
|
560
561
|
page.setDefaultNavigationTimeout(this.options.getPageTimeout)
|
|
561
562
|
this.context = await this.page.$('body')
|
|
562
563
|
if (this.options.browser === 'chrome') {
|
|
563
|
-
await page.bringToFront()
|
|
564
|
+
await page.bringToFront().catch(err => this.debugSection('Page', `bringToFront not supported: ${err.message}`))
|
|
564
565
|
}
|
|
565
566
|
}
|
|
566
567
|
|
|
@@ -1714,6 +1715,10 @@ class Puppeteer extends Helper {
|
|
|
1714
1715
|
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
1715
1716
|
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1716
1717
|
|
|
1718
|
+
// Fuzzy: try radiogroup
|
|
1719
|
+
els = await findByRole(comboboxSearchCtx, { role: 'radiogroup', name: matchedLocator.value })
|
|
1720
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
1721
|
+
|
|
1717
1722
|
// Fuzzy: try native select
|
|
1718
1723
|
const visibleEls = await findVisibleFields.call(this, select, context)
|
|
1719
1724
|
assertElementExists(visibleEls, select, 'Selectable field')
|
|
@@ -1991,7 +1996,7 @@ class Puppeteer extends Helper {
|
|
|
1991
1996
|
const els = await this._locate(locator)
|
|
1992
1997
|
const texts = []
|
|
1993
1998
|
for (const el of els) {
|
|
1994
|
-
texts.push(await (
|
|
1999
|
+
texts.push(await el.evaluate(node => node.innerText))
|
|
1995
2000
|
}
|
|
1996
2001
|
return texts
|
|
1997
2002
|
}
|
|
@@ -2501,7 +2506,6 @@ class Puppeteer extends Helper {
|
|
|
2501
2506
|
*/
|
|
2502
2507
|
async waitInUrl(urlPart, sec = null) {
|
|
2503
2508
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2504
|
-
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2505
2509
|
|
|
2506
2510
|
return this.page
|
|
2507
2511
|
.waitForFunction(
|
|
@@ -2510,12 +2514,12 @@ class Puppeteer extends Helper {
|
|
|
2510
2514
|
return currUrl.indexOf(urlPart) > -1
|
|
2511
2515
|
},
|
|
2512
2516
|
{ timeout: waitTimeout },
|
|
2513
|
-
|
|
2517
|
+
urlPart,
|
|
2514
2518
|
)
|
|
2515
2519
|
.catch(async e => {
|
|
2516
2520
|
const currUrl = await this._getPageUrl()
|
|
2517
2521
|
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2518
|
-
throw new Error(`expected url to include ${
|
|
2522
|
+
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
2519
2523
|
} else {
|
|
2520
2524
|
throw e
|
|
2521
2525
|
}
|
|
@@ -3159,14 +3163,14 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
3159
3163
|
el = await this.context.$('body')
|
|
3160
3164
|
}
|
|
3161
3165
|
|
|
3162
|
-
allText = [await el.
|
|
3166
|
+
allText = [await el.evaluate(node => node.innerText)]
|
|
3163
3167
|
description = 'web application'
|
|
3164
3168
|
} else {
|
|
3165
3169
|
const locator = new Locator(context, 'css')
|
|
3166
3170
|
description = `element ${locator.toString()}`
|
|
3167
3171
|
const els = await this._locate(locator)
|
|
3168
3172
|
assertElementExists(els, locator.toString())
|
|
3169
|
-
allText = await Promise.all(els.map(el => el.
|
|
3173
|
+
allText = await Promise.all(els.map(el => el.evaluate(node => node.innerText)))
|
|
3170
3174
|
}
|
|
3171
3175
|
|
|
3172
3176
|
if (store?.currentStep?.opts?.ignoreCase === true) {
|
|
@@ -3192,8 +3196,19 @@ async function findCheckable(locator, context) {
|
|
|
3192
3196
|
return findElements.call(this, contextEl, matchedLocator)
|
|
3193
3197
|
}
|
|
3194
3198
|
|
|
3199
|
+
// Try ARIA selector for accessible name
|
|
3200
|
+
let els
|
|
3201
|
+
for (const role of checkableRoles) {
|
|
3202
|
+
try {
|
|
3203
|
+
els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`)
|
|
3204
|
+
if (els.length) return els
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
// ARIA selector not supported or failed
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3195
3210
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3196
|
-
|
|
3211
|
+
els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3197
3212
|
if (els.length) {
|
|
3198
3213
|
return els
|
|
3199
3214
|
}
|
|
@@ -3202,14 +3217,6 @@ async function findCheckable(locator, context) {
|
|
|
3202
3217
|
return els
|
|
3203
3218
|
}
|
|
3204
3219
|
|
|
3205
|
-
// Try ARIA selector for accessible name
|
|
3206
|
-
try {
|
|
3207
|
-
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3208
|
-
if (els.length) return els
|
|
3209
|
-
} catch (err) {
|
|
3210
|
-
// ARIA selector not supported or failed
|
|
3211
|
-
}
|
|
3212
|
-
|
|
3213
3220
|
return findElements.call(this, contextEl, matchedLocator.value)
|
|
3214
3221
|
}
|
|
3215
3222
|
|
|
@@ -3456,6 +3463,14 @@ async function targetCreatedHandler(page) {
|
|
|
3456
3463
|
.catch(() => null)
|
|
3457
3464
|
.then(context => (this.context = context))
|
|
3458
3465
|
})
|
|
3466
|
+
page.on('framenavigated', frame => {
|
|
3467
|
+
if (frame.parentFrame()) return
|
|
3468
|
+
if (this.withinLocator) return
|
|
3469
|
+
page
|
|
3470
|
+
.$('body')
|
|
3471
|
+
.catch(() => null)
|
|
3472
|
+
.then(context => (this.context = context))
|
|
3473
|
+
})
|
|
3459
3474
|
page.on('console', msg => {
|
|
3460
3475
|
this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '))
|
|
3461
3476
|
consoleLogStore.add(msg)
|
|
@@ -3656,6 +3671,18 @@ async function proceedSelect(context, el, option) {
|
|
|
3656
3671
|
return this._waitForAction()
|
|
3657
3672
|
}
|
|
3658
3673
|
|
|
3674
|
+
if (role === 'radiogroup') {
|
|
3675
|
+
if (options.length > 1) throw new Error(`selectOption: a radio group holds one value, but ${options.length} options were passed: ${options.join(', ')}`)
|
|
3676
|
+
const [opt] = options
|
|
3677
|
+
let optEls = await findByRole.call(this, el, { role: 'radio', name: opt, exact: true })
|
|
3678
|
+
if (!optEls?.length) optEls = await findByRole.call(this, el, { role: 'radio', name: opt })
|
|
3679
|
+
if (!optEls?.length) throw new ElementNotFound(opt, 'Option', 'was not found in this radio group')
|
|
3680
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3681
|
+
highlightActiveElement.call(this, optEls[0], context)
|
|
3682
|
+
await optEls[0].click()
|
|
3683
|
+
return this._waitForAction()
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3659
3686
|
// Native <select> element
|
|
3660
3687
|
const tagName = await el.evaluate(e => e.tagName)
|
|
3661
3688
|
if (tagName !== 'SELECT') {
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1329,6 +1329,10 @@ class WebDriver extends Helper {
|
|
|
1329
1329
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1330
1330
|
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1331
1331
|
|
|
1332
|
+
// Fuzzy: try radiogroup
|
|
1333
|
+
els = await this._locateByRole({ role: 'radiogroup', text: matchedLocator.value })
|
|
1334
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1335
|
+
|
|
1332
1336
|
// Fuzzy: try native select
|
|
1333
1337
|
const res = await findFields.call(this, select, context)
|
|
1334
1338
|
assertElementExists(res, select, 'Selectable field')
|
|
@@ -2521,7 +2525,6 @@ class WebDriver extends Helper {
|
|
|
2521
2525
|
async waitInUrl(urlPart, sec = null) {
|
|
2522
2526
|
const client = this.browser
|
|
2523
2527
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2524
|
-
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2525
2528
|
let currUrl = ''
|
|
2526
2529
|
|
|
2527
2530
|
return client
|
|
@@ -2529,7 +2532,7 @@ class WebDriver extends Helper {
|
|
|
2529
2532
|
function () {
|
|
2530
2533
|
return this.getUrl().then(res => {
|
|
2531
2534
|
currUrl = decodeUrl(res)
|
|
2532
|
-
return currUrl.indexOf(
|
|
2535
|
+
return currUrl.indexOf(urlPart) > -1
|
|
2533
2536
|
})
|
|
2534
2537
|
},
|
|
2535
2538
|
{ timeout: aSec * 1000 },
|
|
@@ -2537,7 +2540,7 @@ class WebDriver extends Helper {
|
|
|
2537
2540
|
.catch(e => {
|
|
2538
2541
|
e = wrapError(e)
|
|
2539
2542
|
if (e.message.indexOf('timeout')) {
|
|
2540
|
-
throw new Error(`expected url to include ${
|
|
2543
|
+
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
2541
2544
|
}
|
|
2542
2545
|
throw e
|
|
2543
2546
|
})
|
|
@@ -3245,24 +3248,39 @@ async function findCheckable(locator, locateFn) {
|
|
|
3245
3248
|
if (locator.isRole()) return locateFn(locator, true)
|
|
3246
3249
|
if (!locator.isFuzzy()) return locateFn(locator, true)
|
|
3247
3250
|
|
|
3248
|
-
const literal = xpathLocator.literal(locator.value)
|
|
3249
|
-
els = await locateFn(Locator.checkable.byText(literal))
|
|
3250
|
-
if (els.length) return els
|
|
3251
|
-
|
|
3252
3251
|
// Try ARIA selector for accessible name
|
|
3253
3252
|
try {
|
|
3254
|
-
els = await locateFn(`aria/${locator.value}`)
|
|
3253
|
+
els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`))
|
|
3255
3254
|
if (els.length) return els
|
|
3256
3255
|
} catch (e) {
|
|
3257
3256
|
// ARIA selector not supported or failed
|
|
3258
3257
|
}
|
|
3259
3258
|
|
|
3259
|
+
const literal = xpathLocator.literal(locator.value)
|
|
3260
|
+
els = await locateFn(Locator.checkable.byText(literal))
|
|
3261
|
+
if (els.length) return els
|
|
3262
|
+
|
|
3260
3263
|
els = await locateFn(Locator.checkable.byName(literal))
|
|
3261
3264
|
if (els.length) return els
|
|
3262
3265
|
|
|
3263
3266
|
return await locateFn(locator.value) // by css or xpath
|
|
3264
3267
|
}
|
|
3265
3268
|
|
|
3269
|
+
async function keepCheckable(els) {
|
|
3270
|
+
if (!els || !els.length) return []
|
|
3271
|
+
|
|
3272
|
+
const checkable = await this.browser.execute(function () {
|
|
3273
|
+
return Array.prototype.slice.call(arguments).map(function (el) {
|
|
3274
|
+
if (!el) return false
|
|
3275
|
+
const role = el.getAttribute('role')
|
|
3276
|
+
if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1
|
|
3277
|
+
return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')
|
|
3278
|
+
})
|
|
3279
|
+
}, ...els)
|
|
3280
|
+
|
|
3281
|
+
return els.filter((el, index) => checkable[index])
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3266
3284
|
function withStrictLocator(locator) {
|
|
3267
3285
|
locator = new Locator(locator)
|
|
3268
3286
|
return locator.simplify()
|
|
@@ -3562,6 +3580,23 @@ async function proceedSelectOption(elem, option) {
|
|
|
3562
3580
|
return
|
|
3563
3581
|
}
|
|
3564
3582
|
|
|
3583
|
+
if (role === 'radiogroup') {
|
|
3584
|
+
if (options.length > 1) throw new Error(`selectOption: a radio group holds one value, but ${options.length} options were passed: ${options.join(', ')}`)
|
|
3585
|
+
const [opt] = options
|
|
3586
|
+
const radios = await this.browser.findElementsFromElement(elementId, 'xpath', `.//*[@role="radio"]`)
|
|
3587
|
+
const names = []
|
|
3588
|
+
for (const radio of radios) {
|
|
3589
|
+
names.push(await getElementTextAttributes.call(this, radio))
|
|
3590
|
+
}
|
|
3591
|
+
let index = names.findIndex(texts => texts.some(text => text && text.trim() === opt))
|
|
3592
|
+
if (index === -1) index = names.findIndex(texts => texts.some(text => text && text.includes(opt)))
|
|
3593
|
+
if (index === -1) throw new ElementNotFound(opt, 'Option', 'was not found in this radio group')
|
|
3594
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
3595
|
+
highlightActiveElement.call(this, radios[index])
|
|
3596
|
+
await this.browser.elementClick(getElementId(radios[index]))
|
|
3597
|
+
return
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3565
3600
|
// Native <select> element
|
|
3566
3601
|
highlightActiveElement.call(this, elem)
|
|
3567
3602
|
|