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.
Files changed (41) hide show
  1. package/docs/alternative-browsers.md +153 -0
  2. package/docs/basics.md +9 -1
  3. package/docs/configuration.md +2 -0
  4. package/docs/helpers/CDPBrowser.md +2138 -0
  5. package/docs/helpers/Kitesurf.md +118 -0
  6. package/docs/helpers/Obscura.md +210 -0
  7. package/docs/migration-4.md +3 -1
  8. package/docs/parallel.md +10 -0
  9. package/docs/plugins/screencast.md +18 -13
  10. package/docs/plugins.md +1 -1
  11. package/lib/command/info.js +11 -3
  12. package/lib/command/workers/runTests.js +14 -20
  13. package/lib/container.js +6 -0
  14. package/lib/data/context.js +4 -0
  15. package/lib/element/WebElement.js +5 -0
  16. package/lib/helper/Appium.js +14 -2
  17. package/lib/helper/CDPBrowser.js +3004 -0
  18. package/lib/helper/Kitesurf.js +139 -0
  19. package/lib/helper/Obscura.js +344 -0
  20. package/lib/helper/Playwright.js +30 -3
  21. package/lib/helper/Puppeteer.js +43 -16
  22. package/lib/helper/WebDriver.js +43 -8
  23. package/lib/helper/clientscripts/cdpBrowserClient.js +486 -0
  24. package/lib/helper/clientscripts/xpathPolyfill.js +31 -0
  25. package/lib/helper/extras/CDPConnection.js +92 -0
  26. package/lib/helper/extras/CDPElementHandle.js +27 -0
  27. package/lib/helper/extras/apngAssembler.js +156 -0
  28. package/lib/html.js +9 -2
  29. package/lib/listener/retryEnhancer.js +2 -1
  30. package/lib/listener/steps.js +8 -0
  31. package/lib/mocha/hooks.js +10 -0
  32. package/lib/parser.js +14 -2
  33. package/lib/plugin/junitReporter.js +17 -1
  34. package/lib/plugin/screencast.js +116 -24
  35. package/lib/step/base.js +15 -3
  36. package/lib/utils/loaderCheck.js +6 -0
  37. package/lib/utils.js +1 -1
  38. package/lib/workers.js +17 -0
  39. package/package.json +4 -1
  40. package/typings/promiseBasedTypes.d.ts +1833 -0
  41. package/typings/types.d.ts +1840 -0
@@ -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 (await el.getProperty('innerText')).jsonValue())
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
- expectedUrl,
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 ${expectedUrl}, but found ${currUrl}`)
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.getProperty('innerText').then(p => p.jsonValue())]
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.getProperty('innerText').then(p => p.jsonValue())))
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
- let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
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') {
@@ -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(expectedUrl) > -1
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 ${expectedUrl}, but found ${currUrl}`)
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