codeceptjs 3.6.10 → 3.7.0-beta.10

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 (127) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +9 -2
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +66 -102
  5. package/lib/ai.js +130 -121
  6. package/lib/assert/empty.js +3 -5
  7. package/lib/assert/equal.js +4 -7
  8. package/lib/assert/include.js +4 -6
  9. package/lib/assert/throws.js +2 -4
  10. package/lib/assert/truth.js +2 -2
  11. package/lib/codecept.js +87 -83
  12. package/lib/command/check.js +186 -0
  13. package/lib/command/configMigrate.js +2 -4
  14. package/lib/command/definitions.js +8 -26
  15. package/lib/command/generate.js +10 -14
  16. package/lib/command/gherkin/snippets.js +10 -8
  17. package/lib/command/gherkin/steps.js +1 -1
  18. package/lib/command/info.js +1 -3
  19. package/lib/command/init.js +8 -12
  20. package/lib/command/interactive.js +2 -2
  21. package/lib/command/list.js +1 -1
  22. package/lib/command/run-multiple.js +12 -35
  23. package/lib/command/run-workers.js +5 -57
  24. package/lib/command/utils.js +5 -6
  25. package/lib/command/workers/runTests.js +68 -232
  26. package/lib/container.js +354 -237
  27. package/lib/data/context.js +10 -13
  28. package/lib/data/dataScenarioConfig.js +8 -8
  29. package/lib/data/dataTableArgument.js +6 -6
  30. package/lib/data/table.js +5 -11
  31. package/lib/effects.js +218 -0
  32. package/lib/els.js +158 -0
  33. package/lib/event.js +19 -17
  34. package/lib/heal.js +88 -80
  35. package/lib/helper/AI.js +2 -1
  36. package/lib/helper/ApiDataFactory.js +3 -6
  37. package/lib/helper/Appium.js +45 -51
  38. package/lib/helper/FileSystem.js +3 -3
  39. package/lib/helper/GraphQLDataFactory.js +3 -3
  40. package/lib/helper/JSONResponse.js +57 -37
  41. package/lib/helper/Nightmare.js +35 -53
  42. package/lib/helper/Playwright.js +211 -252
  43. package/lib/helper/Protractor.js +54 -77
  44. package/lib/helper/Puppeteer.js +139 -232
  45. package/lib/helper/REST.js +5 -17
  46. package/lib/helper/TestCafe.js +21 -44
  47. package/lib/helper/WebDriver.js +131 -169
  48. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  49. package/lib/listener/emptyRun.js +55 -0
  50. package/lib/listener/exit.js +7 -10
  51. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  52. package/lib/listener/globalTimeout.js +165 -0
  53. package/lib/listener/helpers.js +15 -15
  54. package/lib/listener/mocha.js +1 -1
  55. package/lib/listener/result.js +12 -0
  56. package/lib/listener/steps.js +20 -18
  57. package/lib/listener/store.js +20 -0
  58. package/lib/mocha/asyncWrapper.js +216 -0
  59. package/lib/{interfaces → mocha}/bdd.js +3 -3
  60. package/lib/mocha/cli.js +308 -0
  61. package/lib/mocha/factory.js +104 -0
  62. package/lib/{interfaces → mocha}/featureConfig.js +24 -12
  63. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  64. package/lib/mocha/hooks.js +112 -0
  65. package/lib/mocha/index.js +12 -0
  66. package/lib/mocha/inject.js +29 -0
  67. package/lib/{interfaces → mocha}/scenarioConfig.js +21 -6
  68. package/lib/mocha/suite.js +81 -0
  69. package/lib/mocha/test.js +159 -0
  70. package/lib/mocha/types.d.ts +42 -0
  71. package/lib/mocha/ui.js +219 -0
  72. package/lib/output.js +82 -62
  73. package/lib/pause.js +155 -138
  74. package/lib/plugin/analyze.js +349 -0
  75. package/lib/plugin/autoDelay.js +6 -6
  76. package/lib/plugin/autoLogin.js +6 -7
  77. package/lib/plugin/commentStep.js +6 -1
  78. package/lib/plugin/coverage.js +10 -19
  79. package/lib/plugin/customLocator.js +3 -3
  80. package/lib/plugin/customReporter.js +52 -0
  81. package/lib/plugin/eachElement.js +1 -1
  82. package/lib/plugin/fakerTransform.js +1 -1
  83. package/lib/plugin/heal.js +36 -9
  84. package/lib/plugin/pageInfo.js +140 -0
  85. package/lib/plugin/retryFailedStep.js +4 -4
  86. package/lib/plugin/retryTo.js +18 -118
  87. package/lib/plugin/screenshotOnFail.js +17 -49
  88. package/lib/plugin/selenoid.js +15 -35
  89. package/lib/plugin/standardActingHelpers.js +4 -1
  90. package/lib/plugin/stepByStepReport.js +56 -17
  91. package/lib/plugin/stepTimeout.js +5 -12
  92. package/lib/plugin/subtitles.js +4 -4
  93. package/lib/plugin/tryTo.js +17 -107
  94. package/lib/plugin/wdio.js +8 -10
  95. package/lib/recorder.js +146 -125
  96. package/lib/rerun.js +43 -42
  97. package/lib/result.js +161 -0
  98. package/lib/secret.js +1 -1
  99. package/lib/step/base.js +228 -0
  100. package/lib/step/config.js +50 -0
  101. package/lib/step/func.js +46 -0
  102. package/lib/step/helper.js +50 -0
  103. package/lib/step/meta.js +99 -0
  104. package/lib/step/record.js +74 -0
  105. package/lib/step/retry.js +11 -0
  106. package/lib/step/section.js +55 -0
  107. package/lib/step.js +21 -332
  108. package/lib/steps.js +50 -0
  109. package/lib/store.js +10 -2
  110. package/lib/template/heal.js +2 -11
  111. package/lib/timeout.js +66 -0
  112. package/lib/utils.js +317 -216
  113. package/lib/within.js +73 -55
  114. package/lib/workers.js +259 -275
  115. package/package.json +56 -54
  116. package/typings/index.d.ts +175 -186
  117. package/typings/promiseBasedTypes.d.ts +164 -17
  118. package/typings/types.d.ts +284 -115
  119. package/lib/cli.js +0 -256
  120. package/lib/helper/ExpectHelper.js +0 -391
  121. package/lib/helper/SoftExpectHelper.js +0 -381
  122. package/lib/listener/artifacts.js +0 -19
  123. package/lib/listener/timeout.js +0 -109
  124. package/lib/mochaFactory.js +0 -113
  125. package/lib/plugin/debugErrors.js +0 -67
  126. package/lib/scenario.js +0 -224
  127. package/lib/ui.js +0 -236
@@ -7,6 +7,7 @@ const assert = require('assert')
7
7
  const promiseRetry = require('promise-retry')
8
8
  const Locator = require('../locator')
9
9
  const recorder = require('../recorder')
10
+ const store = require('../store')
10
11
  const stringIncludes = require('../assert/include').includes
11
12
  const { urlEquals } = require('../assert/equal')
12
13
  const { equals } = require('../assert/equal')
@@ -24,6 +25,7 @@ const {
24
25
  clearString,
25
26
  requireWithFallback,
26
27
  normalizeSpacesInString,
28
+ relativeDir,
27
29
  } = require('../utils')
28
30
  const { isColorProperty, convertColorToRGBA } = require('../colorUtils')
29
31
  const ElementNotFound = require('./errors/ElementNotFound')
@@ -40,26 +42,10 @@ const popupStore = new Popup()
40
42
  const consoleLogStore = new Console()
41
43
  const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
42
44
 
43
- const {
44
- setRestartStrategy,
45
- restartsSession,
46
- restartsContext,
47
- restartsBrowser,
48
- } = require('./extras/PlaywrightRestartOpts')
45
+ const { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts')
49
46
  const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine')
50
- const {
51
- seeElementError,
52
- dontSeeElementError,
53
- dontSeeElementInDOMError,
54
- seeElementInDOMError,
55
- } = require('./errors/ElementAssertion')
56
- const {
57
- dontSeeTraffic,
58
- seeTraffic,
59
- grabRecordedNetworkTraffics,
60
- stopRecordingTraffic,
61
- flushNetworkTraffics,
62
- } = require('./network/actions')
47
+ const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
48
+ const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
63
49
 
64
50
  const pathSeparator = path.sep
65
51
 
@@ -392,9 +378,7 @@ class Playwright extends Helper {
392
378
  config = Object.assign(defaults, config)
393
379
 
394
380
  if (availableBrowsers.indexOf(config.browser) < 0) {
395
- throw new Error(
396
- `Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`,
397
- )
381
+ throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`)
398
382
  }
399
383
 
400
384
  return config
@@ -440,9 +424,7 @@ class Playwright extends Helper {
440
424
  }
441
425
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
442
426
  this.isElectron = this.options.browser === 'electron'
443
- this.userDataDir = this.playwrightOptions.userDataDir
444
- ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}`
445
- : undefined
427
+ this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined
446
428
  this.isCDPConnection = this.playwrightOptions.cdpConnection
447
429
  popupStore.defaultAction = this.options.defaultPopupAction
448
430
  }
@@ -458,14 +440,14 @@ class Playwright extends Helper {
458
440
  name: 'url',
459
441
  message: 'Base url of site to be tested',
460
442
  default: 'http://localhost',
461
- when: (answers) => answers.Playwright_browser !== 'electron',
443
+ when: answers => answers.Playwright_browser !== 'electron',
462
444
  },
463
445
  {
464
446
  name: 'show',
465
447
  message: 'Show browser window',
466
448
  default: true,
467
449
  type: 'confirm',
468
- when: (answers) => answers.Playwright_browser !== 'electron',
450
+ when: answers => answers.Playwright_browser !== 'electron',
469
451
  },
470
452
  ]
471
453
  }
@@ -500,9 +482,10 @@ class Playwright extends Helper {
500
482
 
501
483
  async _before(test) {
502
484
  this.currentRunningTest = test
485
+
503
486
  recorder.retry({
504
487
  retries: process.env.FAILED_STEP_RETRIES || 3,
505
- when: (err) => {
488
+ when: err => {
506
489
  if (!err || typeof err.message !== 'string') {
507
490
  return false
508
491
  }
@@ -540,12 +523,17 @@ class Playwright extends Helper {
540
523
  this.currentRunningTest.artifacts.har = fileName
541
524
  contextOptions.recordHar = this.options.recordHar
542
525
  }
526
+
527
+ // load pre-saved cookies
528
+ if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
529
+
543
530
  if (this.storageState) contextOptions.storageState = this.storageState
544
531
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
545
532
  if (this.options.locale) contextOptions.locale = this.options.locale
546
533
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
547
534
  this.contextOptions = contextOptions
548
535
  if (!this.browserContext || !restartsSession()) {
536
+ this.debugSection('New Session', JSON.stringify(this.contextOptions))
549
537
  this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
550
538
  }
551
539
  }
@@ -559,10 +547,7 @@ class Playwright extends Helper {
559
547
  mainPage = existingPages[0] || (await this.browserContext.newPage())
560
548
  } catch (e) {
561
549
  if (this.playwrightOptions.userDataDir) {
562
- this.browser = await playwright[this.options.browser].launchPersistentContext(
563
- this.userDataDir,
564
- this.playwrightOptions,
565
- )
550
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
566
551
  this.browserContext = this.browser
567
552
  const existingPages = await this.browserContext.pages()
568
553
  mainPage = existingPages[0]
@@ -573,6 +558,15 @@ class Playwright extends Helper {
573
558
 
574
559
  await this._setPage(mainPage)
575
560
 
561
+ try {
562
+ // set metadata for reporting
563
+ test.meta.browser = this.browser.browserType().name()
564
+ test.meta.browserVersion = this.browser.version()
565
+ test.meta.windowSize = `${this.page.viewportSize().width}x${this.page.viewportSize().height}`
566
+ } catch (e) {
567
+ this.debug('Failed to set metadata for reporting')
568
+ }
569
+
576
570
  if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true })
577
571
 
578
572
  return this.browser
@@ -583,7 +577,7 @@ class Playwright extends Helper {
583
577
 
584
578
  if (this.isElectron) {
585
579
  this.browser.close()
586
- this.electronSessions.forEach((session) => session.close())
580
+ this.electronSessions.forEach(session => session.close())
587
581
  return
588
582
  }
589
583
 
@@ -605,7 +599,7 @@ class Playwright extends Helper {
605
599
  this.storageState = await currentContext.storageState()
606
600
  }
607
601
 
608
- await Promise.all(contexts.map((c) => c.close()))
602
+ await Promise.all(contexts.map(c => c.close()))
609
603
  }
610
604
  } catch (e) {
611
605
  console.log(e)
@@ -641,10 +635,7 @@ class Playwright extends Helper {
641
635
  page = await browserContext.newPage()
642
636
  } catch (e) {
643
637
  if (this.playwrightOptions.userDataDir) {
644
- browserContext = await playwright[this.options.browser].launchPersistentContext(
645
- `${this.userDataDir}_${this.activeSessionName}`,
646
- this.playwrightOptions,
647
- )
638
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
648
639
  this.browser = browserContext
649
640
  page = await browserContext.pages()[0]
650
641
  }
@@ -660,7 +651,7 @@ class Playwright extends Helper {
660
651
  stop: async () => {
661
652
  // is closed by _after
662
653
  },
663
- loadVars: async (context) => {
654
+ loadVars: async context => {
664
655
  if (context) {
665
656
  this.browserContext = context
666
657
  const existingPages = await context.pages()
@@ -668,7 +659,7 @@ class Playwright extends Helper {
668
659
  return this._setPage(this.sessionPages[this.activeSessionName])
669
660
  }
670
661
  },
671
- restoreVars: async (session) => {
662
+ restoreVars: async session => {
672
663
  this.withinLocator = null
673
664
  this.browserContext = defaultContext
674
665
 
@@ -793,7 +784,7 @@ class Playwright extends Helper {
793
784
  return
794
785
  }
795
786
  page.removeAllListeners('dialog')
796
- page.on('dialog', async (dialog) => {
787
+ page.on('dialog', async dialog => {
797
788
  popupStore.popup = dialog
798
789
  const action = popupStore.actionType || this.options.defaultPopupAction
799
790
  await this._waitForAction()
@@ -856,16 +847,13 @@ class Playwright extends Helper {
856
847
  throw err
857
848
  }
858
849
  } else if (this.playwrightOptions.userDataDir) {
859
- this.browser = await playwright[this.options.browser].launchPersistentContext(
860
- this.userDataDir,
861
- this.playwrightOptions,
862
- )
850
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
863
851
  } else {
864
852
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions)
865
853
  }
866
854
 
867
855
  // works only for Chromium
868
- this.browser.on('targetchanged', (target) => {
856
+ this.browser.on('targetchanged', target => {
869
857
  this.debugSection('Url', target.url())
870
858
  })
871
859
 
@@ -940,7 +928,7 @@ class Playwright extends Helper {
940
928
  const navigationStart = timing.navigationStart
941
929
 
942
930
  const extractedData = {}
943
- dataNames.forEach((name) => {
931
+ dataNames.forEach(name => {
944
932
  extractedData[name] = timing[name] - navigationStart
945
933
  })
946
934
 
@@ -955,7 +943,8 @@ class Playwright extends Helper {
955
943
  throw new Error('Cannot open pages inside an Electron container')
956
944
  }
957
945
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
958
- url = this.options.url + (url.startsWith('/') ? url : `/${url}`)
946
+ url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
947
+ this.debug(`Changed URL to base url + relative path: ${url}`)
959
948
  }
960
949
 
961
950
  if (this.options.basicAuth && this.isAuthenticated !== true) {
@@ -969,13 +958,7 @@ class Playwright extends Helper {
969
958
 
970
959
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
971
960
 
972
- perfTiming = this._extractDataFromPerformanceTiming(
973
- performanceTiming,
974
- 'responseEnd',
975
- 'domInteractive',
976
- 'domContentLoadedEventEnd',
977
- 'loadEventEnd',
978
- )
961
+ perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
979
962
 
980
963
  return this._waitForAction()
981
964
  }
@@ -1197,10 +1180,7 @@ class Playwright extends Helper {
1197
1180
  return this.executeScript(() => {
1198
1181
  const body = document.body
1199
1182
  const html = document.documentElement
1200
- window.scrollTo(
1201
- 0,
1202
- Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
1203
- )
1183
+ window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
1204
1184
  })
1205
1185
  }
1206
1186
 
@@ -1287,7 +1267,22 @@ class Playwright extends Helper {
1287
1267
 
1288
1268
  if (this.frame) return findElements(this.frame, locator)
1289
1269
 
1290
- return findElements(context, locator)
1270
+ const els = await findElements(context, locator)
1271
+
1272
+ if (store.debugMode) {
1273
+ const previewElements = els.slice(0, 3)
1274
+ let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))
1275
+ if (els.length > 3) htmls.push('...')
1276
+ if (els.length > 1) {
1277
+ this.debugSection(`Elements (${els.length})`, htmls.join('|').trim())
1278
+ } else if (els.length === 1) {
1279
+ this.debugSection('Element', htmls.join('|').trim())
1280
+ } else {
1281
+ this.debug(`No elements found by ${JSON.stringify(locator).slice(0, 50)}....`)
1282
+ }
1283
+ }
1284
+
1285
+ return els
1291
1286
  }
1292
1287
 
1293
1288
  /**
@@ -1437,10 +1432,10 @@ class Playwright extends Helper {
1437
1432
  */
1438
1433
  async closeOtherTabs() {
1439
1434
  const pages = await this.browserContext.pages()
1440
- const otherPages = pages.filter((page) => page !== this.page)
1435
+ const otherPages = pages.filter(page => page !== this.page)
1441
1436
  if (otherPages.length) {
1442
1437
  this.debug(`Closing ${otherPages.length} tabs`)
1443
- return Promise.all(otherPages.map((p) => p.close()))
1438
+ return Promise.all(otherPages.map(p => p.close()))
1444
1439
  }
1445
1440
  return Promise.resolve()
1446
1441
  }
@@ -1483,9 +1478,9 @@ class Playwright extends Helper {
1483
1478
  */
1484
1479
  async seeElement(locator) {
1485
1480
  let els = await this._locate(locator)
1486
- els = await Promise.all(els.map((el) => el.isVisible()))
1481
+ els = await Promise.all(els.map(el => el.isVisible()))
1487
1482
  try {
1488
- return empty('visible elements').negate(els.filter((v) => v).fill('ELEMENT'))
1483
+ return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
1489
1484
  } catch (e) {
1490
1485
  dontSeeElementError(locator)
1491
1486
  }
@@ -1497,9 +1492,9 @@ class Playwright extends Helper {
1497
1492
  */
1498
1493
  async dontSeeElement(locator) {
1499
1494
  let els = await this._locate(locator)
1500
- els = await Promise.all(els.map((el) => el.isVisible()))
1495
+ els = await Promise.all(els.map(el => el.isVisible()))
1501
1496
  try {
1502
- return empty('visible elements').assert(els.filter((v) => v).fill('ELEMENT'))
1497
+ return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
1503
1498
  } catch (e) {
1504
1499
  seeElementError(locator)
1505
1500
  }
@@ -1511,7 +1506,7 @@ class Playwright extends Helper {
1511
1506
  async seeElementInDOM(locator) {
1512
1507
  const els = await this._locate(locator)
1513
1508
  try {
1514
- return empty('elements on page').negate(els.filter((v) => v).fill('ELEMENT'))
1509
+ return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
1515
1510
  } catch (e) {
1516
1511
  dontSeeElementInDOMError(locator)
1517
1512
  }
@@ -1523,7 +1518,7 @@ class Playwright extends Helper {
1523
1518
  async dontSeeElementInDOM(locator) {
1524
1519
  const els = await this._locate(locator)
1525
1520
  try {
1526
- return empty('elements on a page').assert(els.filter((v) => v).fill('ELEMENT'))
1521
+ return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
1527
1522
  } catch (e) {
1528
1523
  seeElementInDOMError(locator)
1529
1524
  }
@@ -1547,7 +1542,7 @@ class Playwright extends Helper {
1547
1542
  * @return {Promise<void>}
1548
1543
  */
1549
1544
  async handleDownloads(fileName) {
1550
- this.page.waitForEvent('download').then(async (download) => {
1545
+ this.page.waitForEvent('download').then(async download => {
1551
1546
  const filePath = await download.path()
1552
1547
  fileName = fileName || `downloads/${path.basename(filePath)}`
1553
1548
 
@@ -1741,6 +1736,7 @@ class Playwright extends Helper {
1741
1736
  const el = els[0]
1742
1737
 
1743
1738
  await el.clear()
1739
+ if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
1744
1740
 
1745
1741
  await highlightActiveElement.call(this, el)
1746
1742
 
@@ -1852,8 +1848,8 @@ class Playwright extends Helper {
1852
1848
  */
1853
1849
  async grabNumberOfVisibleElements(locator) {
1854
1850
  let els = await this._locate(locator)
1855
- els = await Promise.all(els.map((el) => el.isVisible()))
1856
- return els.filter((v) => v).length
1851
+ els = await Promise.all(els.map(el => el.isVisible()))
1852
+ return els.filter(v => v).length
1857
1853
  }
1858
1854
 
1859
1855
  /**
@@ -1963,9 +1959,7 @@ class Playwright extends Helper {
1963
1959
  */
1964
1960
  async seeNumberOfElements(locator, num) {
1965
1961
  const elements = await this._locate(locator)
1966
- return equals(
1967
- `expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`,
1968
- ).assert(elements.length, num)
1962
+ return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
1969
1963
  }
1970
1964
 
1971
1965
  /**
@@ -1975,10 +1969,7 @@ class Playwright extends Helper {
1975
1969
  */
1976
1970
  async seeNumberOfVisibleElements(locator, num) {
1977
1971
  const res = await this.grabNumberOfVisibleElements(locator)
1978
- return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(
1979
- res,
1980
- num,
1981
- )
1972
+ return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
1982
1973
  }
1983
1974
 
1984
1975
  /**
@@ -1997,7 +1988,7 @@ class Playwright extends Helper {
1997
1988
  */
1998
1989
  async seeCookie(name) {
1999
1990
  const cookies = await this.browserContext.cookies()
2000
- empty(`cookie ${name} to be set`).negate(cookies.filter((c) => c.name === name))
1991
+ empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
2001
1992
  }
2002
1993
 
2003
1994
  /**
@@ -2005,7 +1996,7 @@ class Playwright extends Helper {
2005
1996
  */
2006
1997
  async dontSeeCookie(name) {
2007
1998
  const cookies = await this.browserContext.cookies()
2008
- empty(`cookie ${name} not to be set`).assert(cookies.filter((c) => c.name === name))
1999
+ empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
2009
2000
  }
2010
2001
 
2011
2002
  /**
@@ -2016,17 +2007,18 @@ class Playwright extends Helper {
2016
2007
  async grabCookie(name) {
2017
2008
  const cookies = await this.browserContext.cookies()
2018
2009
  if (!name) return cookies
2019
- const cookie = cookies.filter((c) => c.name === name)
2010
+ const cookie = cookies.filter(c => c.name === name)
2020
2011
  if (cookie[0]) return cookie[0]
2021
2012
  }
2022
2013
 
2023
2014
  /**
2024
2015
  * {{> clearCookie }}
2025
2016
  */
2026
- async clearCookie() {
2027
- // Playwright currently doesn't support to delete a certain cookie
2028
- // https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies
2017
+ async clearCookie(cookieName) {
2029
2018
  if (!this.browserContext) return
2019
+ if (cookieName) {
2020
+ return this.browserContext.clearCookies({ name: cookieName })
2021
+ }
2030
2022
  return this.browserContext.clearCookies()
2031
2023
  }
2032
2024
 
@@ -2098,7 +2090,7 @@ class Playwright extends Helper {
2098
2090
  const els = await this._locate(locator)
2099
2091
  const texts = []
2100
2092
  for (const el of els) {
2101
- texts.push(await await el.innerText())
2093
+ texts.push(await el.innerText())
2102
2094
  }
2103
2095
  this.debug(`Matched ${els.length} elements`)
2104
2096
  return texts
@@ -2120,7 +2112,7 @@ class Playwright extends Helper {
2120
2112
  async grabValueFromAll(locator) {
2121
2113
  const els = await findFields.call(this, locator)
2122
2114
  this.debug(`Matched ${els.length} elements`)
2123
- return Promise.all(els.map((el) => el.inputValue()))
2115
+ return Promise.all(els.map(el => el.inputValue()))
2124
2116
  }
2125
2117
 
2126
2118
  /**
@@ -2139,7 +2131,7 @@ class Playwright extends Helper {
2139
2131
  async grabHTMLFromAll(locator) {
2140
2132
  const els = await this._locate(locator)
2141
2133
  this.debug(`Matched ${els.length} elements`)
2142
- return Promise.all(els.map((el) => el.innerHTML()))
2134
+ return Promise.all(els.map(el => el.innerHTML()))
2143
2135
  }
2144
2136
 
2145
2137
  /**
@@ -2160,11 +2152,7 @@ class Playwright extends Helper {
2160
2152
  async grabCssPropertyFromAll(locator, cssProperty) {
2161
2153
  const els = await this._locate(locator)
2162
2154
  this.debug(`Matched ${els.length} elements`)
2163
- const cssValues = await Promise.all(
2164
- els.map((el) =>
2165
- el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty),
2166
- ),
2167
- )
2155
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2168
2156
 
2169
2157
  return cssValues
2170
2158
  }
@@ -2192,19 +2180,16 @@ class Playwright extends Helper {
2192
2180
  }
2193
2181
  }
2194
2182
 
2195
- const values = Object.keys(cssPropertiesCamelCase).map((key) => cssPropertiesCamelCase[key])
2183
+ const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
2196
2184
  if (!Array.isArray(props)) props = [props]
2197
2185
  let chunked = chunkArray(props, values.length)
2198
- chunked = chunked.filter((val) => {
2186
+ chunked = chunked.filter(val => {
2199
2187
  for (let i = 0; i < val.length; ++i) {
2200
- // eslint-disable-next-line eqeqeq
2201
2188
  if (val[i] != values[i]) return false
2202
2189
  }
2203
2190
  return true
2204
2191
  })
2205
- return equals(
2206
- `all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
2207
- ).assert(chunked.length, elemAmount)
2192
+ return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
2208
2193
  }
2209
2194
 
2210
2195
  /**
@@ -2217,16 +2202,16 @@ class Playwright extends Helper {
2217
2202
 
2218
2203
  const elemAmount = res.length
2219
2204
  const commands = []
2220
- res.forEach((el) => {
2221
- Object.keys(attributes).forEach((prop) => {
2205
+ res.forEach(el => {
2206
+ Object.keys(attributes).forEach(prop => {
2222
2207
  commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
2223
2208
  })
2224
2209
  })
2225
2210
  let attrs = await Promise.all(commands)
2226
- const values = Object.keys(attributes).map((key) => attributes[key])
2211
+ const values = Object.keys(attributes).map(key => attributes[key])
2227
2212
  if (!Array.isArray(attrs)) attrs = [attrs]
2228
2213
  let chunked = chunkArray(attrs, values.length)
2229
- chunked = chunked.filter((val) => {
2214
+ chunked = chunked.filter(val => {
2230
2215
  for (let i = 0; i < val.length; ++i) {
2231
2216
  // the attribute could be a boolean
2232
2217
  if (typeof val[i] === 'boolean') return val[i] === values[i]
@@ -2235,10 +2220,7 @@ class Playwright extends Helper {
2235
2220
  }
2236
2221
  return true
2237
2222
  })
2238
- return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
2239
- chunked.length,
2240
- elemAmount,
2241
- )
2223
+ return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
2242
2224
  }
2243
2225
 
2244
2226
  /**
@@ -2311,7 +2293,7 @@ class Playwright extends Helper {
2311
2293
  const fullPageOption = fullPage || this.options.fullPageScreenshots
2312
2294
  let outputFile = screenshotOutputFolder(fileName)
2313
2295
 
2314
- this.debug(`Screenshot is saving to ${outputFile}`)
2296
+ this.debugSection('Screenshot', relativeDir(outputFile))
2315
2297
 
2316
2298
  await this.page.screenshot({
2317
2299
  path: outputFile,
@@ -2324,7 +2306,7 @@ class Playwright extends Helper {
2324
2306
  const activeSessionPage = this.sessionPages[sessionName]
2325
2307
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2326
2308
 
2327
- this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2309
+ this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2328
2310
 
2329
2311
  if (activeSessionPage) {
2330
2312
  await activeSessionPage.screenshot({
@@ -2358,9 +2340,7 @@ class Playwright extends Helper {
2358
2340
  method = method.toLowerCase()
2359
2341
  const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
2360
2342
  if (!allowedMethods.includes(method)) {
2361
- throw new Error(
2362
- `Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`,
2363
- )
2343
+ throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
2364
2344
  }
2365
2345
 
2366
2346
  if (url.startsWith('/')) {
@@ -2397,10 +2377,7 @@ class Playwright extends Helper {
2397
2377
  if (this.options.recordVideo && this.page && this.page.video()) {
2398
2378
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2399
2379
  for (const sessionName in this.sessionPages) {
2400
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2401
- this.sessionPages[sessionName],
2402
- `${test.title}_${sessionName}.failed`,
2403
- )
2380
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
2404
2381
  }
2405
2382
  }
2406
2383
 
@@ -2408,10 +2385,7 @@ class Playwright extends Helper {
2408
2385
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2409
2386
  for (const sessionName in this.sessionPages) {
2410
2387
  if (!this.sessionPages[sessionName].context) continue
2411
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2412
- this.sessionPages[sessionName].context,
2413
- `${test.title}_${sessionName}.failed`,
2414
- )
2388
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
2415
2389
  }
2416
2390
  }
2417
2391
 
@@ -2425,16 +2399,13 @@ class Playwright extends Helper {
2425
2399
  if (this.options.keepVideoForPassedTests) {
2426
2400
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2427
2401
  for (const sessionName of Object.keys(this.sessionPages)) {
2428
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2429
- this.sessionPages[sessionName],
2430
- `${test.title}_${sessionName}.passed`,
2431
- )
2402
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
2432
2403
  }
2433
2404
  } else {
2434
2405
  this.page
2435
2406
  .video()
2436
2407
  .delete()
2437
- .catch((e) => {})
2408
+ .catch(e => {})
2438
2409
  }
2439
2410
  }
2440
2411
 
@@ -2444,10 +2415,7 @@ class Playwright extends Helper {
2444
2415
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2445
2416
  for (const sessionName in this.sessionPages) {
2446
2417
  if (!this.sessionPages[sessionName].context) continue
2447
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2448
- this.sessionPages[sessionName].context,
2449
- `${test.title}_${sessionName}.passed`,
2450
- )
2418
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
2451
2419
  }
2452
2420
  }
2453
2421
  } else {
@@ -2464,7 +2432,7 @@ class Playwright extends Helper {
2464
2432
  * {{> wait }}
2465
2433
  */
2466
2434
  async wait(sec) {
2467
- return new Promise((done) => {
2435
+ return new Promise(done => {
2468
2436
  setTimeout(done, sec * 1000)
2469
2437
  })
2470
2438
  }
@@ -2480,20 +2448,18 @@ class Playwright extends Helper {
2480
2448
  const context = await this._getContext()
2481
2449
  if (!locator.isXPath()) {
2482
2450
  const valueFn = function ([locator]) {
2483
- return Array.from(document.querySelectorAll(locator)).filter((el) => !el.disabled).length > 0
2451
+ return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0
2484
2452
  }
2485
2453
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2486
2454
  } else {
2487
2455
  const enabledFn = function ([locator, $XPath]) {
2488
- eval($XPath) // eslint-disable-line no-eval
2489
- return $XPath(null, locator).filter((el) => !el.disabled).length > 0
2456
+ eval($XPath)
2457
+ return $XPath(null, locator).filter(el => !el.disabled).length > 0
2490
2458
  }
2491
2459
  waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2492
2460
  }
2493
- return waiter.catch((err) => {
2494
- throw new Error(
2495
- `element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2496
- )
2461
+ return waiter.catch(err => {
2462
+ throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2497
2463
  })
2498
2464
  }
2499
2465
 
@@ -2508,20 +2474,18 @@ class Playwright extends Helper {
2508
2474
  const context = await this._getContext()
2509
2475
  if (!locator.isXPath()) {
2510
2476
  const valueFn = function ([locator]) {
2511
- return Array.from(document.querySelectorAll(locator)).filter((el) => el.disabled).length > 0
2477
+ return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0
2512
2478
  }
2513
2479
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2514
2480
  } else {
2515
2481
  const disabledFn = function ([locator, $XPath]) {
2516
- eval($XPath) // eslint-disable-line no-eval
2517
- return $XPath(null, locator).filter((el) => el.disabled).length > 0
2482
+ eval($XPath)
2483
+ return $XPath(null, locator).filter(el => el.disabled).length > 0
2518
2484
  }
2519
2485
  waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2520
2486
  }
2521
- return waiter.catch((err) => {
2522
- throw new Error(
2523
- `element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2524
- )
2487
+ return waiter.catch(err => {
2488
+ throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2525
2489
  })
2526
2490
  }
2527
2491
 
@@ -2536,26 +2500,21 @@ class Playwright extends Helper {
2536
2500
  const context = await this._getContext()
2537
2501
  if (!locator.isXPath()) {
2538
2502
  const valueFn = function ([locator, value]) {
2539
- return (
2540
- Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
2541
- 0
2542
- )
2503
+ return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2543
2504
  }
2544
2505
  waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
2545
2506
  } else {
2546
2507
  const valueFn = function ([locator, $XPath, value]) {
2547
- eval($XPath) // eslint-disable-line no-eval
2548
- return $XPath(null, locator).filter((el) => (el.value || '').indexOf(value) !== -1).length > 0
2508
+ eval($XPath)
2509
+ return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2549
2510
  }
2550
2511
  waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
2551
2512
  timeout: waitTimeout,
2552
2513
  })
2553
2514
  }
2554
- return waiter.catch((err) => {
2515
+ return waiter.catch(err => {
2555
2516
  const loc = locator.toString()
2556
- throw new Error(
2557
- `element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`,
2558
- )
2517
+ throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`)
2559
2518
  })
2560
2519
  }
2561
2520
 
@@ -2575,22 +2534,20 @@ class Playwright extends Helper {
2575
2534
  if (!els || els.length === 0) {
2576
2535
  return false
2577
2536
  }
2578
- return Array.prototype.filter.call(els, (el) => el.offsetParent !== null).length === num
2537
+ return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
2579
2538
  }
2580
2539
  waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
2581
2540
  } else {
2582
2541
  const visibleFn = function ([locator, $XPath, num]) {
2583
- eval($XPath) // eslint-disable-line no-eval
2584
- return $XPath(null, locator).filter((el) => el.offsetParent !== null).length === num
2542
+ eval($XPath)
2543
+ return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
2585
2544
  }
2586
2545
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
2587
2546
  timeout: waitTimeout,
2588
2547
  })
2589
2548
  }
2590
- return waiter.catch((err) => {
2591
- throw new Error(
2592
- `The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`,
2593
- )
2549
+ return waiter.catch(err => {
2550
+ throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
2594
2551
  })
2595
2552
  }
2596
2553
 
@@ -2598,9 +2555,7 @@ class Playwright extends Helper {
2598
2555
  * {{> waitForClickable }}
2599
2556
  */
2600
2557
  async waitForClickable(locator, waitTimeout) {
2601
- console.log(
2602
- 'I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable',
2603
- )
2558
+ console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
2604
2559
  console.log('Remove usage of this function')
2605
2560
  }
2606
2561
 
@@ -2616,9 +2571,7 @@ class Playwright extends Helper {
2616
2571
  try {
2617
2572
  await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2618
2573
  } catch (e) {
2619
- throw new Error(
2620
- `element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
2621
- )
2574
+ throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2622
2575
  }
2623
2576
  }
2624
2577
 
@@ -2710,10 +2663,8 @@ class Playwright extends Helper {
2710
2663
  .locator(buildLocatorString(locator))
2711
2664
  .first()
2712
2665
  .waitFor({ timeout: waitTimeout, state: 'hidden' })
2713
- .catch((err) => {
2714
- throw new Error(
2715
- `element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`,
2716
- )
2666
+ .catch(err => {
2667
+ throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
2717
2668
  })
2718
2669
  }
2719
2670
 
@@ -2739,6 +2690,9 @@ class Playwright extends Helper {
2739
2690
  if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
2740
2691
  return this.context
2741
2692
  }
2693
+ if (this.frame) {
2694
+ return this.frame
2695
+ }
2742
2696
  return this.page
2743
2697
  }
2744
2698
 
@@ -2750,14 +2704,14 @@ class Playwright extends Helper {
2750
2704
 
2751
2705
  return this.page
2752
2706
  .waitForFunction(
2753
- (urlPart) => {
2707
+ urlPart => {
2754
2708
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2755
2709
  return currUrl.indexOf(urlPart) > -1
2756
2710
  },
2757
2711
  urlPart,
2758
2712
  { timeout: waitTimeout },
2759
2713
  )
2760
- .catch(async (e) => {
2714
+ .catch(async e => {
2761
2715
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2762
2716
  if (/Timeout/i.test(e.message)) {
2763
2717
  throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
@@ -2780,14 +2734,14 @@ class Playwright extends Helper {
2780
2734
 
2781
2735
  return this.page
2782
2736
  .waitForFunction(
2783
- (urlPart) => {
2737
+ urlPart => {
2784
2738
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2785
2739
  return currUrl.indexOf(urlPart) > -1
2786
2740
  },
2787
2741
  urlPart,
2788
2742
  { timeout: waitTimeout },
2789
2743
  )
2790
- .catch(async (e) => {
2744
+ .catch(async e => {
2791
2745
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2792
2746
  if (/Timeout/i.test(e.message)) {
2793
2747
  throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
@@ -2803,28 +2757,23 @@ class Playwright extends Helper {
2803
2757
  async waitForText(text, sec = null, context = null) {
2804
2758
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2805
2759
  const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
2806
- let waiter
2807
2760
 
2808
2761
  const contextObject = await this._getContext()
2809
2762
 
2810
2763
  if (context) {
2811
2764
  const locator = new Locator(context, 'css')
2812
- if (!locator.isXPath()) {
2813
- try {
2814
- await contextObject
2765
+ try {
2766
+ if (!locator.isXPath()) {
2767
+ return contextObject
2815
2768
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2816
2769
  .first()
2817
2770
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2818
- } catch (e) {
2819
- throw new Error(`${errorMessage}\n${e.message}`)
2820
2771
  }
2821
- }
2822
2772
 
2823
- if (locator.isXPath()) {
2824
- try {
2825
- await contextObject.waitForFunction(
2773
+ if (locator.isXPath()) {
2774
+ return contextObject.waitForFunction(
2826
2775
  ([locator, text, $XPath]) => {
2827
- eval($XPath) // eslint-disable-line no-eval
2776
+ eval($XPath)
2828
2777
  const el = $XPath(null, locator)
2829
2778
  if (!el.length) return false
2830
2779
  return el[0].innerText.indexOf(text) > -1
@@ -2832,27 +2781,34 @@ class Playwright extends Helper {
2832
2781
  [locator.value, text, $XPath.toString()],
2833
2782
  { timeout: waitTimeout },
2834
2783
  )
2835
- } catch (e) {
2836
- throw new Error(`${errorMessage}\n${e.message}`)
2837
2784
  }
2785
+ } catch (e) {
2786
+ throw new Error(`${errorMessage}\n${e.message}`)
2838
2787
  }
2839
- } else {
2840
- // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
2841
-
2842
- const _contextObject = this.frame ? this.frame : contextObject
2843
- let count = 0
2844
- do {
2845
- waiter = await _contextObject
2846
- .locator(`:has-text(${JSON.stringify(text)})`)
2847
- .first()
2848
- .isVisible()
2849
- if (waiter) break
2850
- await this.wait(1)
2851
- count += 1000
2852
- } while (count <= waitTimeout)
2853
-
2854
- if (!waiter) throw new Error(`${errorMessage}`)
2855
2788
  }
2789
+
2790
+ const timeoutGap = waitTimeout + 1000
2791
+
2792
+ // We add basic timeout to make sure we don't wait forever
2793
+ // We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
2794
+ // or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
2795
+ // If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
2796
+ return Promise.race([
2797
+ new Promise((_, reject) => {
2798
+ setTimeout(() => reject(errorMessage), waitTimeout)
2799
+ }),
2800
+ this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2801
+ promiseRetry(
2802
+ async retry => {
2803
+ const textPresent = await contextObject
2804
+ .locator(`:has-text(${JSON.stringify(text)})`)
2805
+ .first()
2806
+ .isVisible()
2807
+ if (!textPresent) retry(errorMessage)
2808
+ },
2809
+ { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
2810
+ ),
2811
+ ])
2856
2812
  }
2857
2813
 
2858
2814
  /**
@@ -3021,11 +2977,11 @@ class Playwright extends Helper {
3021
2977
  }
3022
2978
  } else {
3023
2979
  const visibleFn = function ([locator, $XPath]) {
3024
- eval($XPath) // eslint-disable-line no-eval
2980
+ eval($XPath)
3025
2981
  return $XPath(null, locator).length === 0
3026
2982
  }
3027
2983
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
3028
- return waiter.catch((err) => {
2984
+ return waiter.catch(err => {
3029
2985
  throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
3030
2986
  })
3031
2987
  }
@@ -3047,9 +3003,9 @@ class Playwright extends Helper {
3047
3003
 
3048
3004
  return promiseRetry(
3049
3005
  async (retry, number) => {
3050
- const _grabCookie = async (name) => {
3006
+ const _grabCookie = async name => {
3051
3007
  const cookies = await this.browserContext.cookies()
3052
- const cookie = cookies.filter((c) => c.name === name)
3008
+ const cookie = cookies.filter(c => c.name === name)
3053
3009
  if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
3054
3010
  }
3055
3011
 
@@ -3128,7 +3084,7 @@ class Playwright extends Helper {
3128
3084
  this.recording = true
3129
3085
  this.recordedAtLeastOnce = true
3130
3086
 
3131
- this.page.on('requestfinished', async (request) => {
3087
+ this.page.on('requestfinished', async request => {
3132
3088
  const information = {
3133
3089
  url: request.url(),
3134
3090
  method: request.method(),
@@ -3167,20 +3123,20 @@ class Playwright extends Helper {
3167
3123
  */
3168
3124
  blockTraffic(urls) {
3169
3125
  if (Array.isArray(urls)) {
3170
- urls.forEach((url) => {
3171
- this.page.route(url, (route) => {
3126
+ urls.forEach(url => {
3127
+ this.page.route(url, route => {
3172
3128
  route
3173
3129
  .abort()
3174
3130
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3175
- .catch((e) => {})
3131
+ .catch(e => {})
3176
3132
  })
3177
3133
  })
3178
3134
  } else {
3179
- this.page.route(urls, (route) => {
3135
+ this.page.route(urls, route => {
3180
3136
  route
3181
3137
  .abort()
3182
3138
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3183
- .catch((e) => {})
3139
+ .catch(e => {})
3184
3140
  })
3185
3141
  }
3186
3142
  }
@@ -3209,8 +3165,8 @@ class Playwright extends Helper {
3209
3165
  urls = [urls]
3210
3166
  }
3211
3167
 
3212
- urls.forEach((url) => {
3213
- this.page.route(url, (route) => {
3168
+ urls.forEach(url => {
3169
+ this.page.route(url, route => {
3214
3170
  if (this.page.isClosed()) {
3215
3171
  // Sometimes it happens that browser has been closed in the meantime.
3216
3172
  // In this case we just don't fulfill to prevent error in test scenario.
@@ -3256,13 +3212,10 @@ class Playwright extends Helper {
3256
3212
  */
3257
3213
  grabTrafficUrl(urlMatch) {
3258
3214
  if (!this.recordedAtLeastOnce) {
3259
- throw new Error(
3260
- 'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
3261
- )
3215
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
3262
3216
  }
3263
3217
 
3264
3218
  for (const i in this.requests) {
3265
- // eslint-disable-next-line no-prototype-builtins
3266
3219
  if (this.requests.hasOwnProperty(i)) {
3267
3220
  const request = this.requests[i]
3268
3221
 
@@ -3312,15 +3265,15 @@ class Playwright extends Helper {
3312
3265
  await this.cdpSession.send('Network.enable')
3313
3266
  await this.cdpSession.send('Page.enable')
3314
3267
 
3315
- this.cdpSession.on('Network.webSocketFrameReceived', (payload) => {
3268
+ this.cdpSession.on('Network.webSocketFrameReceived', payload => {
3316
3269
  this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
3317
3270
  })
3318
3271
 
3319
- this.cdpSession.on('Network.webSocketFrameSent', (payload) => {
3272
+ this.cdpSession.on('Network.webSocketFrameSent', payload => {
3320
3273
  this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
3321
3274
  })
3322
3275
 
3323
- this.cdpSession.on('Network.webSocketFrameError', (payload) => {
3276
+ this.cdpSession.on('Network.webSocketFrameError', payload => {
3324
3277
  this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
3325
3278
  })
3326
3279
  }
@@ -3344,9 +3297,7 @@ class Playwright extends Helper {
3344
3297
  grabWebSocketMessages() {
3345
3298
  if (!this.recordingWebSocketMessages) {
3346
3299
  if (!this.recordedWebSocketMessagesAtLeastOnce) {
3347
- throw new Error(
3348
- 'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
3349
- )
3300
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
3350
3301
  }
3351
3302
  }
3352
3303
  return this.webSocketMessages
@@ -3493,17 +3444,13 @@ async function proceedClick(locator, context = null, options = {}) {
3493
3444
  }
3494
3445
  const els = await findClickable.call(this, matcher, locator)
3495
3446
  if (context) {
3496
- assertElementExists(
3497
- els,
3498
- locator,
3499
- 'Clickable element',
3500
- `was not found inside element ${new Locator(context).toString()}`,
3501
- )
3447
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
3502
3448
  } else {
3503
3449
  assertElementExists(els, locator, 'Clickable element')
3504
3450
  }
3505
3451
 
3506
3452
  await highlightActiveElement.call(this, els[0])
3453
+ if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
3507
3454
 
3508
3455
  /*
3509
3456
  using the force true options itself but instead dispatching a click
@@ -3565,16 +3512,18 @@ async function proceedSee(assertType, text, context, strict = false) {
3565
3512
  description = `element ${locator.toString()}`
3566
3513
  const els = await this._locate(locator)
3567
3514
  assertElementExists(els, locator.toString())
3568
- allText = await Promise.all(els.map((el) => el.innerText()))
3515
+ allText = await Promise.all(els.map(el => el.innerText()))
3516
+ }
3517
+
3518
+ if (store?.currentStep?.opts?.ignoreCase === true) {
3519
+ text = text.toLowerCase()
3520
+ allText = allText.map(elText => elText.toLowerCase())
3569
3521
  }
3570
3522
 
3571
3523
  if (strict) {
3572
- return allText.map((elText) => equals(description)[assertType](text, elText))
3524
+ return allText.map(elText => equals(description)[assertType](text, elText))
3573
3525
  }
3574
- return stringIncludes(description)[assertType](
3575
- normalizeSpacesInString(text),
3576
- normalizeSpacesInString(allText.join(' | ')),
3577
- )
3526
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
3578
3527
  }
3579
3528
 
3580
3529
  async function findCheckable(locator, context) {
@@ -3604,7 +3553,7 @@ async function findCheckable(locator, context) {
3604
3553
  async function proceedIsChecked(assertType, option) {
3605
3554
  let els = await findCheckable.call(this, option)
3606
3555
  assertElementExists(els, option, 'Checkable')
3607
- els = await Promise.all(els.map((el) => el.isChecked()))
3556
+ els = await Promise.all(els.map(el => el.isChecked()))
3608
3557
  const selected = els.reduce((prev, cur) => prev || cur)
3609
3558
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3610
3559
  }
@@ -3636,10 +3585,10 @@ async function proceedSeeInField(assertType, field, value) {
3636
3585
  const els = await findFields.call(this, field)
3637
3586
  assertElementExists(els, field, 'Field')
3638
3587
  const el = els[0]
3639
- const tag = await el.evaluate((e) => e.tagName)
3588
+ const tag = await el.evaluate(e => e.tagName)
3640
3589
  const fieldType = await el.getAttribute('type')
3641
3590
 
3642
- const proceedMultiple = async (elements) => {
3591
+ const proceedMultiple = async elements => {
3643
3592
  const fields = Array.isArray(elements) ? elements : [elements]
3644
3593
 
3645
3594
  const elementValues = []
@@ -3653,7 +3602,7 @@ async function proceedSeeInField(assertType, field, value) {
3653
3602
  if (assertType === 'assert') {
3654
3603
  equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
3655
3604
  }
3656
- elementValues.forEach((val) => stringIncludes(`fields by ${field}`)[assertType](value, val))
3605
+ elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
3657
3606
  }
3658
3607
  }
3659
3608
 
@@ -3740,6 +3689,8 @@ function isFrameLocator(locator) {
3740
3689
  }
3741
3690
 
3742
3691
  function assertElementExists(res, locator, prefix, suffix) {
3692
+ // if element text is an empty string, just exit this check
3693
+ if (typeof res === 'string' && res === '') return
3743
3694
  if (!res || res.length === 0) {
3744
3695
  throw new ElementNotFound(locator, prefix, suffix)
3745
3696
  }
@@ -3776,12 +3727,9 @@ async function targetCreatedHandler(page) {
3776
3727
  this.contextLocator = null
3777
3728
  })
3778
3729
  })
3779
- page.on('console', (msg) => {
3730
+ page.on('console', msg => {
3780
3731
  if (!consoleLogStore.includes(msg) && this.options.ignoreLog && !this.options.ignoreLog.includes(msg.type())) {
3781
- this.debugSection(
3782
- `Browser:${ucfirst(msg.type())}`,
3783
- ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '),
3784
- )
3732
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
3785
3733
  }
3786
3734
  consoleLogStore.add(msg)
3787
3735
  })
@@ -3889,7 +3837,7 @@ async function refreshContextSession() {
3889
3837
  const contexts = await this.browser.contexts()
3890
3838
  contexts.shift()
3891
3839
 
3892
- await Promise.all(contexts.map((c) => c.close()))
3840
+ await Promise.all(contexts.map(c => c.close()))
3893
3841
  } catch (e) {
3894
3842
  console.log(e)
3895
3843
  }
@@ -3908,10 +3856,10 @@ async function refreshContextSession() {
3908
3856
  const currentUrl = await this.grabCurrentUrl()
3909
3857
 
3910
3858
  if (currentUrl.startsWith('http')) {
3911
- await this.executeScript('localStorage.clear();').catch((err) => {
3859
+ await this.executeScript('localStorage.clear();').catch(err => {
3912
3860
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3913
3861
  })
3914
- await this.executeScript('sessionStorage.clear();').catch((err) => {
3862
+ await this.executeScript('sessionStorage.clear();').catch(err => {
3915
3863
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3916
3864
  })
3917
3865
  }
@@ -3941,11 +3889,22 @@ async function saveTraceForContext(context, name) {
3941
3889
  }
3942
3890
 
3943
3891
  async function highlightActiveElement(element) {
3944
- if (this.options.highlightElement && global.debugMode) {
3945
- await element.evaluate((el) => {
3892
+ if ((this.options.highlightElement || store.onPause) && store.debugMode) {
3893
+ await element.evaluate(el => {
3946
3894
  const prevStyle = el.style.boxShadow
3947
- el.style.boxShadow = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)'
3895
+ el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
3948
3896
  setTimeout(() => (el.style.boxShadow = prevStyle), 2000)
3949
3897
  })
3950
3898
  }
3951
3899
  }
3900
+
3901
+ async function elToString(el, numberOfElements) {
3902
+ const html = await el.evaluate(node => node.outerHTML)
3903
+ return (
3904
+ html
3905
+ .replace(/\n/g, '')
3906
+ .replace(/\s+/g, ' ')
3907
+ .substring(0, 100 / numberOfElements)
3908
+ .trim() + '...'
3909
+ )
3910
+ }