codeceptjs 4.0.0-rc.1 → 4.0.0-rc.7

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 (36) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +610 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  12. package/docs/webapi/seeElement.mustache +4 -0
  13. package/docs/webapi/seeInField.mustache +5 -0
  14. package/docs/webapi/selectOption.mustache +5 -0
  15. package/docs/webapi/uncheckOption.mustache +1 -1
  16. package/lib/codecept.js +20 -17
  17. package/lib/command/init.js +0 -3
  18. package/lib/command/run-workers.js +1 -0
  19. package/lib/container.js +19 -4
  20. package/lib/helper/Appium.js +8 -8
  21. package/lib/helper/Playwright.js +163 -70
  22. package/lib/helper/Puppeteer.js +165 -59
  23. package/lib/helper/WebDriver.js +134 -49
  24. package/lib/listener/globalRetry.js +32 -6
  25. package/lib/plugin/aiTrace.js +464 -0
  26. package/lib/plugin/retryFailedStep.js +28 -19
  27. package/lib/plugin/stepByStepReport.js +5 -1
  28. package/lib/utils.js +48 -0
  29. package/lib/workers.js +49 -7
  30. package/package.json +5 -3
  31. package/lib/listener/enhancedGlobalRetry.js +0 -110
  32. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  33. package/lib/plugin/htmlReporter.js +0 -3648
  34. package/lib/retryCoordinator.js +0 -207
  35. package/typings/promiseBasedTypes.d.ts +0 -9469
  36. package/typings/types.d.ts +0 -11402
@@ -26,6 +26,10 @@ import {
26
26
  isModifierKey,
27
27
  requireWithFallback,
28
28
  normalizeSpacesInString,
29
+ normalizePath,
30
+ resolveUrl,
31
+ getMimeType,
32
+ base64EncodeFile,
29
33
  } from '../utils.js'
30
34
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
35
  import ElementNotFound from './errors/ElementNotFound.js'
@@ -1158,8 +1162,16 @@ class Puppeteer extends Helper {
1158
1162
  * {{> seeElement }}
1159
1163
  * {{ react }}
1160
1164
  */
1161
- async seeElement(locator) {
1162
- let els = await this._locate(locator)
1165
+ async seeElement(locator, context = null) {
1166
+ let els
1167
+ if (context) {
1168
+ const contextPage = await this.context
1169
+ const contextEls = await findElements.call(this, contextPage, context)
1170
+ assertElementExists(contextEls, context, 'Context element')
1171
+ els = await findElements.call(this, contextEls[0], locator)
1172
+ } else {
1173
+ els = await this._locate(locator)
1174
+ }
1163
1175
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1164
1176
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1165
1177
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1174,8 +1186,16 @@ class Puppeteer extends Helper {
1174
1186
  * {{> dontSeeElement }}
1175
1187
  * {{ react }}
1176
1188
  */
1177
- async dontSeeElement(locator) {
1178
- let els = await this._locate(locator)
1189
+ async dontSeeElement(locator, context = null) {
1190
+ let els
1191
+ if (context) {
1192
+ const contextPage = await this.context
1193
+ const contextEls = await findElements.call(this, contextPage, context)
1194
+ assertElementExists(contextEls, context, 'Context element')
1195
+ els = await findElements.call(this, contextEls[0], locator)
1196
+ } else {
1197
+ els = await this._locate(locator)
1198
+ }
1179
1199
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1180
1200
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1181
1201
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1541,8 +1561,8 @@ class Puppeteer extends Helper {
1541
1561
  * {{> fillField }}
1542
1562
  * {{ react }}
1543
1563
  */
1544
- async fillField(field, value) {
1545
- const els = await findVisibleFields.call(this, field)
1564
+ async fillField(field, value, context = null) {
1565
+ const els = await findVisibleFields.call(this, field, context)
1546
1566
  assertElementExists(els, field, 'Field')
1547
1567
  const el = els[0]
1548
1568
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
@@ -1562,8 +1582,8 @@ class Puppeteer extends Helper {
1562
1582
  /**
1563
1583
  * {{> clearField }}
1564
1584
  */
1565
- async clearField(field) {
1566
- return this.fillField(field, '')
1585
+ async clearField(field, context = null) {
1586
+ return this.fillField(field, '', context)
1567
1587
  }
1568
1588
 
1569
1589
  /**
@@ -1571,8 +1591,8 @@ class Puppeteer extends Helper {
1571
1591
  *
1572
1592
  * {{ react }}
1573
1593
  */
1574
- async appendField(field, value) {
1575
- const els = await findVisibleFields.call(this, field)
1594
+ async appendField(field, value, context = null) {
1595
+ const els = await findVisibleFields.call(this, field, context)
1576
1596
  assertElementExists(els, field, 'Field')
1577
1597
  highlightActiveElement.call(this, els[0], await this._getContext())
1578
1598
  await els[0].press('End')
@@ -1583,17 +1603,17 @@ class Puppeteer extends Helper {
1583
1603
  /**
1584
1604
  * {{> seeInField }}
1585
1605
  */
1586
- async seeInField(field, value) {
1606
+ async seeInField(field, value, context = null) {
1587
1607
  const _value = typeof value === 'boolean' ? value : value.toString()
1588
- return proceedSeeInField.call(this, 'assert', field, _value)
1608
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
1589
1609
  }
1590
1610
 
1591
1611
  /**
1592
1612
  * {{> dontSeeInField }}
1593
1613
  */
1594
- async dontSeeInField(field, value) {
1614
+ async dontSeeInField(field, value, context = null) {
1595
1615
  const _value = typeof value === 'boolean' ? value : value.toString()
1596
- return proceedSeeInField.call(this, 'negate', field, _value)
1616
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
1597
1617
  }
1598
1618
 
1599
1619
  /**
@@ -1601,46 +1621,77 @@ class Puppeteer extends Helper {
1601
1621
  *
1602
1622
  * {{> attachFile }}
1603
1623
  */
1604
- async attachFile(locator, pathToFile) {
1624
+ async attachFile(locator, pathToFile, context = null) {
1605
1625
  const file = path.join(global.codecept_dir, pathToFile)
1606
1626
 
1607
1627
  if (!fileExists(file)) {
1608
1628
  throw new Error(`File at ${file} can not be found on local system`)
1609
1629
  }
1610
- const els = await findFields.call(this, locator)
1611
- assertElementExists(els, locator, 'Field')
1612
- await els[0].uploadFile(file)
1630
+ const els = await findFields.call(this, locator, context)
1631
+ if (els.length) {
1632
+ const tag = await els[0].evaluate(el => el.tagName)
1633
+ const type = await els[0].evaluate(el => el.type)
1634
+ if (tag === 'INPUT' && type === 'file') {
1635
+ await els[0].uploadFile(file)
1636
+ return this._waitForAction()
1637
+ }
1638
+ }
1639
+
1640
+ const targetEls = els.length ? els : await this._locate(locator)
1641
+ assertElementExists(targetEls, locator, 'Element')
1642
+ const base64Content = base64EncodeFile(file)
1643
+ const fileName = path.basename(file)
1644
+ const mimeType = getMimeType(fileName)
1645
+ await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
1646
+ const binaryStr = atob(base64Content)
1647
+ const bytes = new Uint8Array(binaryStr.length)
1648
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
1649
+ const fileObj = new File([bytes], fileName, { type: mimeType })
1650
+ const dataTransfer = new DataTransfer()
1651
+ dataTransfer.items.add(fileObj)
1652
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
1653
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
1654
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
1655
+ }, { base64Content, fileName, mimeType })
1613
1656
  return this._waitForAction()
1614
1657
  }
1615
1658
 
1616
1659
  /**
1617
1660
  * {{> selectOption }}
1618
1661
  */
1619
- async selectOption(select, option) {
1620
- const context = await this._getContext()
1662
+ async selectOption(select, option, context = null) {
1663
+ const pageContext = await this._getContext()
1621
1664
  const matchedLocator = new Locator(select)
1622
1665
 
1666
+ let contextEl
1667
+ if (context) {
1668
+ const contextEls = await findElements.call(this, pageContext, context)
1669
+ assertElementExists(contextEls, context, 'Context element')
1670
+ contextEl = contextEls[0]
1671
+ }
1672
+
1623
1673
  // Strict locator
1624
1674
  if (!matchedLocator.isFuzzy()) {
1625
1675
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1626
- const els = await this._locate(select)
1676
+ const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1627
1677
  assertElementExists(els, select, 'Selectable element')
1628
- return proceedSelect.call(this, context, els[0], option)
1678
+ return proceedSelect.call(this, pageContext, els[0], option)
1629
1679
  }
1630
1680
 
1631
1681
  // Fuzzy: try combobox
1632
1682
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1633
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
1634
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1683
+ const comboboxSearchCtx = contextEl || pageContext
1684
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1685
+ if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
1635
1686
 
1636
1687
  // Fuzzy: try listbox
1637
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
1638
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1688
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1689
+ if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
1639
1690
 
1640
1691
  // Fuzzy: try native select
1641
- const visibleEls = await findVisibleFields.call(this, select)
1692
+ const visibleEls = await findVisibleFields.call(this, select, context)
1642
1693
  assertElementExists(visibleEls, select, 'Selectable field')
1643
- return proceedSelect.call(this, context, visibleEls[0], option)
1694
+ return proceedSelect.call(this, pageContext, visibleEls[0], option)
1644
1695
  }
1645
1696
 
1646
1697
  /**
@@ -1684,6 +1735,26 @@ class Puppeteer extends Helper {
1684
1735
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
1685
1736
  }
1686
1737
 
1738
+ /**
1739
+ * {{> seeCurrentPathEquals }}
1740
+ */
1741
+ async seeCurrentPathEquals(path) {
1742
+ const currentUrl = await this._getPageUrl()
1743
+ const baseUrl = this.options.url || 'http://localhost'
1744
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1745
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1746
+ }
1747
+
1748
+ /**
1749
+ * {{> dontSeeCurrentPathEquals }}
1750
+ */
1751
+ async dontSeeCurrentPathEquals(path) {
1752
+ const currentUrl = await this._getPageUrl()
1753
+ const baseUrl = this.options.url || 'http://localhost'
1754
+ const actualPath = new URL(currentUrl, baseUrl).pathname
1755
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1756
+ }
1757
+
1687
1758
  /**
1688
1759
  * {{> see }}
1689
1760
  *
@@ -2421,6 +2492,7 @@ class Puppeteer extends Helper {
2421
2492
  */
2422
2493
  async waitInUrl(urlPart, sec = null) {
2423
2494
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2495
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2424
2496
 
2425
2497
  return this.page
2426
2498
  .waitForFunction(
@@ -2429,12 +2501,12 @@ class Puppeteer extends Helper {
2429
2501
  return currUrl.indexOf(urlPart) > -1
2430
2502
  },
2431
2503
  { timeout: waitTimeout },
2432
- urlPart,
2504
+ expectedUrl,
2433
2505
  )
2434
2506
  .catch(async e => {
2435
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2507
+ const currUrl = await this._getPageUrl()
2436
2508
  if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2437
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2509
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2438
2510
  } else {
2439
2511
  throw e
2440
2512
  }
@@ -2446,18 +2518,13 @@ class Puppeteer extends Helper {
2446
2518
  */
2447
2519
  async waitUrlEquals(urlPart, sec = null) {
2448
2520
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2449
-
2450
- const baseUrl = this.options.url
2451
- let expectedUrl = urlPart
2452
- if (urlPart.indexOf('http') < 0) {
2453
- expectedUrl = baseUrl + urlPart
2454
- }
2521
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2455
2522
 
2456
2523
  return this.page
2457
2524
  .waitForFunction(
2458
2525
  url => {
2459
2526
  const currUrl = decodeURIComponent(window.location.href)
2460
- return currUrl.indexOf(url) > -1
2527
+ return currUrl === url
2461
2528
  },
2462
2529
  { timeout: waitTimeout },
2463
2530
  expectedUrl,
@@ -2465,11 +2532,36 @@ class Puppeteer extends Helper {
2465
2532
  .catch(async e => {
2466
2533
  const currUrl = await this._getPageUrl()
2467
2534
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2468
- if (!currUrl.includes(expectedUrl)) {
2469
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2470
- } else {
2471
- throw new Error(`expected url not loaded, error message: ${e.message}`)
2472
- }
2535
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2536
+ } else {
2537
+ throw e
2538
+ }
2539
+ })
2540
+ }
2541
+
2542
+ /**
2543
+ * {{> waitCurrentPathEquals }}
2544
+ */
2545
+ async waitCurrentPathEquals(path, sec = null) {
2546
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2547
+ const normalizedPath = normalizePath(path)
2548
+
2549
+ return this.page
2550
+ .waitForFunction(
2551
+ expectedPath => {
2552
+ const actualPath = window.location.pathname
2553
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
2554
+ return normalizePath(actualPath) === expectedPath
2555
+ },
2556
+ { timeout: waitTimeout },
2557
+ normalizedPath,
2558
+ )
2559
+ .catch(async e => {
2560
+ const currUrl = await this._getPageUrl()
2561
+ const baseUrl = this.options.url || 'http://localhost'
2562
+ const actualPath = new URL(currUrl, baseUrl).pathname
2563
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2564
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2473
2565
  } else {
2474
2566
  throw e
2475
2567
  }
@@ -3140,43 +3232,57 @@ async function proceedIsChecked(assertType, option) {
3140
3232
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3141
3233
  }
3142
3234
 
3143
- async function findVisibleFields(locator) {
3144
- const els = await findFields.call(this, locator)
3235
+ async function findVisibleFields(locator, context = null) {
3236
+ const els = await findFields.call(this, locator, context)
3145
3237
  const visible = await Promise.all(els.map(el => el.boundingBox()))
3146
3238
  return els.filter((el, index) => visible[index])
3147
3239
  }
3148
3240
 
3149
- async function findFields(locator) {
3241
+ async function findFields(locator, context = null) {
3242
+ let contextEl
3243
+ if (context) {
3244
+ const contextPage = await this.context
3245
+ const contextEls = await findElements.call(this, contextPage, context)
3246
+ assertElementExists(contextEls, context, 'Context element')
3247
+ contextEl = contextEls[0]
3248
+ }
3249
+
3250
+ const locateFn = contextEl
3251
+ ? loc => findElements.call(this, contextEl, loc)
3252
+ : loc => this._locate(loc)
3253
+
3150
3254
  const matchedLocator = new Locator(locator)
3151
3255
  if (!matchedLocator.isFuzzy()) {
3152
- return this._locate(matchedLocator)
3256
+ return locateFn(matchedLocator)
3153
3257
  }
3154
3258
  const literal = xpathLocator.literal(matchedLocator.value)
3155
3259
 
3156
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
3260
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
3157
3261
  if (els.length) {
3158
3262
  return els
3159
3263
  }
3160
3264
 
3161
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
3265
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
3162
3266
  if (els.length) {
3163
3267
  return els
3164
3268
  }
3165
- els = await this._locate({ xpath: Locator.field.byName(literal) })
3269
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
3166
3270
  if (els.length) {
3167
3271
  return els
3168
3272
  }
3169
3273
 
3170
3274
  // Try ARIA selector for accessible name
3171
- try {
3172
- const page = await this.context
3173
- els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3174
- if (els.length) return els
3175
- } catch (err) {
3176
- // ARIA selector not supported or failed
3275
+ if (!contextEl) {
3276
+ try {
3277
+ const page = await this.context
3278
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3279
+ if (els.length) return els
3280
+ } catch (err) {
3281
+ // ARIA selector not supported or failed
3282
+ }
3177
3283
  }
3178
3284
 
3179
- return this._locate({ css: matchedLocator.value })
3285
+ return locateFn({ css: matchedLocator.value })
3180
3286
  }
3181
3287
 
3182
3288
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -3205,8 +3311,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3205
3311
  await this._waitForAction()
3206
3312
  }
3207
3313
 
3208
- async function proceedSeeInField(assertType, field, value) {
3209
- const els = await findVisibleFields.call(this, field)
3314
+ async function proceedSeeInField(assertType, field, value, context) {
3315
+ const els = await findVisibleFields.call(this, field, context)
3210
3316
  assertElementExists(els, field, 'Field')
3211
3317
  const el = els[0]
3212
3318
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())