codeceptjs 3.6.10 → 3.7.0-beta.2

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 (105) hide show
  1. package/README.md +81 -110
  2. package/bin/codecept.js +2 -2
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +46 -36
  5. package/lib/assert/empty.js +3 -5
  6. package/lib/assert/equal.js +4 -7
  7. package/lib/assert/include.js +4 -6
  8. package/lib/assert/throws.js +2 -4
  9. package/lib/assert/truth.js +2 -2
  10. package/lib/codecept.js +87 -83
  11. package/lib/command/configMigrate.js +2 -4
  12. package/lib/command/definitions.js +5 -25
  13. package/lib/command/generate.js +10 -14
  14. package/lib/command/gherkin/snippets.js +10 -8
  15. package/lib/command/gherkin/steps.js +1 -1
  16. package/lib/command/info.js +1 -3
  17. package/lib/command/init.js +8 -12
  18. package/lib/command/interactive.js +1 -1
  19. package/lib/command/list.js +1 -1
  20. package/lib/command/run-multiple.js +12 -35
  21. package/lib/command/run-workers.js +10 -10
  22. package/lib/command/utils.js +5 -6
  23. package/lib/command/workers/runTests.js +14 -17
  24. package/lib/container.js +327 -237
  25. package/lib/data/context.js +10 -13
  26. package/lib/data/dataScenarioConfig.js +8 -8
  27. package/lib/data/dataTableArgument.js +6 -6
  28. package/lib/data/table.js +5 -11
  29. package/lib/els.js +177 -0
  30. package/lib/event.js +1 -0
  31. package/lib/heal.js +78 -80
  32. package/lib/helper/ApiDataFactory.js +3 -6
  33. package/lib/helper/Appium.js +15 -30
  34. package/lib/helper/FileSystem.js +3 -3
  35. package/lib/helper/GraphQLDataFactory.js +3 -3
  36. package/lib/helper/JSONResponse.js +57 -37
  37. package/lib/helper/Nightmare.js +35 -53
  38. package/lib/helper/Playwright.js +189 -251
  39. package/lib/helper/Protractor.js +54 -77
  40. package/lib/helper/Puppeteer.js +134 -232
  41. package/lib/helper/REST.js +5 -17
  42. package/lib/helper/TestCafe.js +21 -44
  43. package/lib/helper/WebDriver.js +103 -162
  44. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  45. package/lib/listener/artifacts.js +2 -2
  46. package/lib/listener/emptyRun.js +58 -0
  47. package/lib/listener/exit.js +4 -4
  48. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  49. package/lib/listener/{timeout.js → globalTimeout.js} +9 -8
  50. package/lib/listener/helpers.js +15 -15
  51. package/lib/listener/mocha.js +1 -1
  52. package/lib/listener/steps.js +17 -12
  53. package/lib/listener/store.js +12 -0
  54. package/lib/mocha/asyncWrapper.js +204 -0
  55. package/lib/{interfaces → mocha}/bdd.js +3 -3
  56. package/lib/mocha/cli.js +257 -0
  57. package/lib/mocha/factory.js +104 -0
  58. package/lib/{interfaces → mocha}/featureConfig.js +11 -12
  59. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  60. package/lib/mocha/hooks.js +83 -0
  61. package/lib/mocha/index.js +12 -0
  62. package/lib/mocha/inject.js +24 -0
  63. package/lib/{interfaces → mocha}/scenarioConfig.js +10 -6
  64. package/lib/mocha/suite.js +55 -0
  65. package/lib/mocha/test.js +60 -0
  66. package/lib/mocha/types.d.ts +31 -0
  67. package/lib/mocha/ui.js +219 -0
  68. package/lib/output.js +28 -10
  69. package/lib/pause.js +159 -135
  70. package/lib/plugin/autoDelay.js +4 -4
  71. package/lib/plugin/autoLogin.js +6 -7
  72. package/lib/plugin/commentStep.js +1 -1
  73. package/lib/plugin/coverage.js +10 -19
  74. package/lib/plugin/customLocator.js +3 -3
  75. package/lib/plugin/debugErrors.js +2 -2
  76. package/lib/plugin/eachElement.js +1 -1
  77. package/lib/plugin/fakerTransform.js +1 -1
  78. package/lib/plugin/heal.js +6 -9
  79. package/lib/plugin/retryFailedStep.js +4 -4
  80. package/lib/plugin/retryTo.js +2 -2
  81. package/lib/plugin/screenshotOnFail.js +9 -36
  82. package/lib/plugin/selenoid.js +15 -35
  83. package/lib/plugin/stepByStepReport.js +51 -13
  84. package/lib/plugin/stepTimeout.js +4 -11
  85. package/lib/plugin/subtitles.js +4 -4
  86. package/lib/plugin/tryTo.js +1 -1
  87. package/lib/plugin/wdio.js +8 -10
  88. package/lib/recorder.js +142 -121
  89. package/lib/secret.js +1 -1
  90. package/lib/step.js +160 -144
  91. package/lib/store.js +6 -2
  92. package/lib/template/heal.js +2 -11
  93. package/lib/utils.js +224 -216
  94. package/lib/within.js +73 -55
  95. package/lib/workers.js +265 -261
  96. package/package.json +46 -47
  97. package/typings/index.d.ts +172 -184
  98. package/typings/promiseBasedTypes.d.ts +95 -516
  99. package/typings/types.d.ts +169 -587
  100. package/lib/cli.js +0 -256
  101. package/lib/helper/ExpectHelper.js +0 -391
  102. package/lib/helper/SoftExpectHelper.js +0 -381
  103. package/lib/mochaFactory.js +0 -113
  104. package/lib/scenario.js +0 -224
  105. 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
  }
@@ -502,7 +484,7 @@ class Playwright extends Helper {
502
484
  this.currentRunningTest = test
503
485
  recorder.retry({
504
486
  retries: process.env.FAILED_STEP_RETRIES || 3,
505
- when: (err) => {
487
+ when: err => {
506
488
  if (!err || typeof err.message !== 'string') {
507
489
  return false
508
490
  }
@@ -559,10 +541,7 @@ class Playwright extends Helper {
559
541
  mainPage = existingPages[0] || (await this.browserContext.newPage())
560
542
  } catch (e) {
561
543
  if (this.playwrightOptions.userDataDir) {
562
- this.browser = await playwright[this.options.browser].launchPersistentContext(
563
- this.userDataDir,
564
- this.playwrightOptions,
565
- )
544
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
566
545
  this.browserContext = this.browser
567
546
  const existingPages = await this.browserContext.pages()
568
547
  mainPage = existingPages[0]
@@ -583,7 +562,7 @@ class Playwright extends Helper {
583
562
 
584
563
  if (this.isElectron) {
585
564
  this.browser.close()
586
- this.electronSessions.forEach((session) => session.close())
565
+ this.electronSessions.forEach(session => session.close())
587
566
  return
588
567
  }
589
568
 
@@ -605,7 +584,7 @@ class Playwright extends Helper {
605
584
  this.storageState = await currentContext.storageState()
606
585
  }
607
586
 
608
- await Promise.all(contexts.map((c) => c.close()))
587
+ await Promise.all(contexts.map(c => c.close()))
609
588
  }
610
589
  } catch (e) {
611
590
  console.log(e)
@@ -641,10 +620,7 @@ class Playwright extends Helper {
641
620
  page = await browserContext.newPage()
642
621
  } catch (e) {
643
622
  if (this.playwrightOptions.userDataDir) {
644
- browserContext = await playwright[this.options.browser].launchPersistentContext(
645
- `${this.userDataDir}_${this.activeSessionName}`,
646
- this.playwrightOptions,
647
- )
623
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
648
624
  this.browser = browserContext
649
625
  page = await browserContext.pages()[0]
650
626
  }
@@ -660,7 +636,7 @@ class Playwright extends Helper {
660
636
  stop: async () => {
661
637
  // is closed by _after
662
638
  },
663
- loadVars: async (context) => {
639
+ loadVars: async context => {
664
640
  if (context) {
665
641
  this.browserContext = context
666
642
  const existingPages = await context.pages()
@@ -668,7 +644,7 @@ class Playwright extends Helper {
668
644
  return this._setPage(this.sessionPages[this.activeSessionName])
669
645
  }
670
646
  },
671
- restoreVars: async (session) => {
647
+ restoreVars: async session => {
672
648
  this.withinLocator = null
673
649
  this.browserContext = defaultContext
674
650
 
@@ -793,7 +769,7 @@ class Playwright extends Helper {
793
769
  return
794
770
  }
795
771
  page.removeAllListeners('dialog')
796
- page.on('dialog', async (dialog) => {
772
+ page.on('dialog', async dialog => {
797
773
  popupStore.popup = dialog
798
774
  const action = popupStore.actionType || this.options.defaultPopupAction
799
775
  await this._waitForAction()
@@ -856,16 +832,13 @@ class Playwright extends Helper {
856
832
  throw err
857
833
  }
858
834
  } else if (this.playwrightOptions.userDataDir) {
859
- this.browser = await playwright[this.options.browser].launchPersistentContext(
860
- this.userDataDir,
861
- this.playwrightOptions,
862
- )
835
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
863
836
  } else {
864
837
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions)
865
838
  }
866
839
 
867
840
  // works only for Chromium
868
- this.browser.on('targetchanged', (target) => {
841
+ this.browser.on('targetchanged', target => {
869
842
  this.debugSection('Url', target.url())
870
843
  })
871
844
 
@@ -940,7 +913,7 @@ class Playwright extends Helper {
940
913
  const navigationStart = timing.navigationStart
941
914
 
942
915
  const extractedData = {}
943
- dataNames.forEach((name) => {
916
+ dataNames.forEach(name => {
944
917
  extractedData[name] = timing[name] - navigationStart
945
918
  })
946
919
 
@@ -969,13 +942,7 @@ class Playwright extends Helper {
969
942
 
970
943
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
971
944
 
972
- perfTiming = this._extractDataFromPerformanceTiming(
973
- performanceTiming,
974
- 'responseEnd',
975
- 'domInteractive',
976
- 'domContentLoadedEventEnd',
977
- 'loadEventEnd',
978
- )
945
+ perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
979
946
 
980
947
  return this._waitForAction()
981
948
  }
@@ -1197,10 +1164,7 @@ class Playwright extends Helper {
1197
1164
  return this.executeScript(() => {
1198
1165
  const body = document.body
1199
1166
  const html = document.documentElement
1200
- window.scrollTo(
1201
- 0,
1202
- Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
1203
- )
1167
+ window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
1204
1168
  })
1205
1169
  }
1206
1170
 
@@ -1287,7 +1251,22 @@ class Playwright extends Helper {
1287
1251
 
1288
1252
  if (this.frame) return findElements(this.frame, locator)
1289
1253
 
1290
- return findElements(context, locator)
1254
+ const els = await findElements(context, locator)
1255
+
1256
+ if (store.debugMode) {
1257
+ const previewElements = els.slice(0, 3)
1258
+ let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))
1259
+ if (els.length > 3) htmls.push('...')
1260
+ if (els.length > 1) {
1261
+ this.debugSection(`Elements (${els.length})`, htmls.join('|').trim())
1262
+ } else if (els.length === 1) {
1263
+ this.debugSection('Element', htmls.join('|').trim())
1264
+ } else {
1265
+ this.debug(`No elements found by ${JSON.stringify(locator).slice(0, 50)}....`)
1266
+ }
1267
+ }
1268
+
1269
+ return els
1291
1270
  }
1292
1271
 
1293
1272
  /**
@@ -1437,10 +1416,10 @@ class Playwright extends Helper {
1437
1416
  */
1438
1417
  async closeOtherTabs() {
1439
1418
  const pages = await this.browserContext.pages()
1440
- const otherPages = pages.filter((page) => page !== this.page)
1419
+ const otherPages = pages.filter(page => page !== this.page)
1441
1420
  if (otherPages.length) {
1442
1421
  this.debug(`Closing ${otherPages.length} tabs`)
1443
- return Promise.all(otherPages.map((p) => p.close()))
1422
+ return Promise.all(otherPages.map(p => p.close()))
1444
1423
  }
1445
1424
  return Promise.resolve()
1446
1425
  }
@@ -1483,9 +1462,9 @@ class Playwright extends Helper {
1483
1462
  */
1484
1463
  async seeElement(locator) {
1485
1464
  let els = await this._locate(locator)
1486
- els = await Promise.all(els.map((el) => el.isVisible()))
1465
+ els = await Promise.all(els.map(el => el.isVisible()))
1487
1466
  try {
1488
- return empty('visible elements').negate(els.filter((v) => v).fill('ELEMENT'))
1467
+ return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
1489
1468
  } catch (e) {
1490
1469
  dontSeeElementError(locator)
1491
1470
  }
@@ -1497,9 +1476,9 @@ class Playwright extends Helper {
1497
1476
  */
1498
1477
  async dontSeeElement(locator) {
1499
1478
  let els = await this._locate(locator)
1500
- els = await Promise.all(els.map((el) => el.isVisible()))
1479
+ els = await Promise.all(els.map(el => el.isVisible()))
1501
1480
  try {
1502
- return empty('visible elements').assert(els.filter((v) => v).fill('ELEMENT'))
1481
+ return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
1503
1482
  } catch (e) {
1504
1483
  seeElementError(locator)
1505
1484
  }
@@ -1511,7 +1490,7 @@ class Playwright extends Helper {
1511
1490
  async seeElementInDOM(locator) {
1512
1491
  const els = await this._locate(locator)
1513
1492
  try {
1514
- return empty('elements on page').negate(els.filter((v) => v).fill('ELEMENT'))
1493
+ return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
1515
1494
  } catch (e) {
1516
1495
  dontSeeElementInDOMError(locator)
1517
1496
  }
@@ -1523,7 +1502,7 @@ class Playwright extends Helper {
1523
1502
  async dontSeeElementInDOM(locator) {
1524
1503
  const els = await this._locate(locator)
1525
1504
  try {
1526
- return empty('elements on a page').assert(els.filter((v) => v).fill('ELEMENT'))
1505
+ return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
1527
1506
  } catch (e) {
1528
1507
  seeElementInDOMError(locator)
1529
1508
  }
@@ -1547,7 +1526,7 @@ class Playwright extends Helper {
1547
1526
  * @return {Promise<void>}
1548
1527
  */
1549
1528
  async handleDownloads(fileName) {
1550
- this.page.waitForEvent('download').then(async (download) => {
1529
+ this.page.waitForEvent('download').then(async download => {
1551
1530
  const filePath = await download.path()
1552
1531
  fileName = fileName || `downloads/${path.basename(filePath)}`
1553
1532
 
@@ -1741,6 +1720,7 @@ class Playwright extends Helper {
1741
1720
  const el = els[0]
1742
1721
 
1743
1722
  await el.clear()
1723
+ if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
1744
1724
 
1745
1725
  await highlightActiveElement.call(this, el)
1746
1726
 
@@ -1852,8 +1832,8 @@ class Playwright extends Helper {
1852
1832
  */
1853
1833
  async grabNumberOfVisibleElements(locator) {
1854
1834
  let els = await this._locate(locator)
1855
- els = await Promise.all(els.map((el) => el.isVisible()))
1856
- return els.filter((v) => v).length
1835
+ els = await Promise.all(els.map(el => el.isVisible()))
1836
+ return els.filter(v => v).length
1857
1837
  }
1858
1838
 
1859
1839
  /**
@@ -1963,9 +1943,7 @@ class Playwright extends Helper {
1963
1943
  */
1964
1944
  async seeNumberOfElements(locator, num) {
1965
1945
  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)
1946
+ return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
1969
1947
  }
1970
1948
 
1971
1949
  /**
@@ -1975,10 +1953,7 @@ class Playwright extends Helper {
1975
1953
  */
1976
1954
  async seeNumberOfVisibleElements(locator, num) {
1977
1955
  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
- )
1956
+ return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
1982
1957
  }
1983
1958
 
1984
1959
  /**
@@ -1997,7 +1972,7 @@ class Playwright extends Helper {
1997
1972
  */
1998
1973
  async seeCookie(name) {
1999
1974
  const cookies = await this.browserContext.cookies()
2000
- empty(`cookie ${name} to be set`).negate(cookies.filter((c) => c.name === name))
1975
+ empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
2001
1976
  }
2002
1977
 
2003
1978
  /**
@@ -2005,7 +1980,7 @@ class Playwright extends Helper {
2005
1980
  */
2006
1981
  async dontSeeCookie(name) {
2007
1982
  const cookies = await this.browserContext.cookies()
2008
- empty(`cookie ${name} not to be set`).assert(cookies.filter((c) => c.name === name))
1983
+ empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
2009
1984
  }
2010
1985
 
2011
1986
  /**
@@ -2016,17 +1991,18 @@ class Playwright extends Helper {
2016
1991
  async grabCookie(name) {
2017
1992
  const cookies = await this.browserContext.cookies()
2018
1993
  if (!name) return cookies
2019
- const cookie = cookies.filter((c) => c.name === name)
1994
+ const cookie = cookies.filter(c => c.name === name)
2020
1995
  if (cookie[0]) return cookie[0]
2021
1996
  }
2022
1997
 
2023
1998
  /**
2024
1999
  * {{> clearCookie }}
2025
2000
  */
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
2001
+ async clearCookie(cookieName) {
2029
2002
  if (!this.browserContext) return
2003
+ if (cookieName) {
2004
+ return this.browserContext.clearCookies({ name: cookieName })
2005
+ }
2030
2006
  return this.browserContext.clearCookies()
2031
2007
  }
2032
2008
 
@@ -2098,7 +2074,7 @@ class Playwright extends Helper {
2098
2074
  const els = await this._locate(locator)
2099
2075
  const texts = []
2100
2076
  for (const el of els) {
2101
- texts.push(await await el.innerText())
2077
+ texts.push(await el.innerText())
2102
2078
  }
2103
2079
  this.debug(`Matched ${els.length} elements`)
2104
2080
  return texts
@@ -2120,7 +2096,7 @@ class Playwright extends Helper {
2120
2096
  async grabValueFromAll(locator) {
2121
2097
  const els = await findFields.call(this, locator)
2122
2098
  this.debug(`Matched ${els.length} elements`)
2123
- return Promise.all(els.map((el) => el.inputValue()))
2099
+ return Promise.all(els.map(el => el.inputValue()))
2124
2100
  }
2125
2101
 
2126
2102
  /**
@@ -2139,7 +2115,7 @@ class Playwright extends Helper {
2139
2115
  async grabHTMLFromAll(locator) {
2140
2116
  const els = await this._locate(locator)
2141
2117
  this.debug(`Matched ${els.length} elements`)
2142
- return Promise.all(els.map((el) => el.innerHTML()))
2118
+ return Promise.all(els.map(el => el.innerHTML()))
2143
2119
  }
2144
2120
 
2145
2121
  /**
@@ -2160,11 +2136,7 @@ class Playwright extends Helper {
2160
2136
  async grabCssPropertyFromAll(locator, cssProperty) {
2161
2137
  const els = await this._locate(locator)
2162
2138
  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
- )
2139
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2168
2140
 
2169
2141
  return cssValues
2170
2142
  }
@@ -2192,19 +2164,16 @@ class Playwright extends Helper {
2192
2164
  }
2193
2165
  }
2194
2166
 
2195
- const values = Object.keys(cssPropertiesCamelCase).map((key) => cssPropertiesCamelCase[key])
2167
+ const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
2196
2168
  if (!Array.isArray(props)) props = [props]
2197
2169
  let chunked = chunkArray(props, values.length)
2198
- chunked = chunked.filter((val) => {
2170
+ chunked = chunked.filter(val => {
2199
2171
  for (let i = 0; i < val.length; ++i) {
2200
- // eslint-disable-next-line eqeqeq
2201
2172
  if (val[i] != values[i]) return false
2202
2173
  }
2203
2174
  return true
2204
2175
  })
2205
- return equals(
2206
- `all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
2207
- ).assert(chunked.length, elemAmount)
2176
+ return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
2208
2177
  }
2209
2178
 
2210
2179
  /**
@@ -2217,16 +2186,16 @@ class Playwright extends Helper {
2217
2186
 
2218
2187
  const elemAmount = res.length
2219
2188
  const commands = []
2220
- res.forEach((el) => {
2221
- Object.keys(attributes).forEach((prop) => {
2189
+ res.forEach(el => {
2190
+ Object.keys(attributes).forEach(prop => {
2222
2191
  commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
2223
2192
  })
2224
2193
  })
2225
2194
  let attrs = await Promise.all(commands)
2226
- const values = Object.keys(attributes).map((key) => attributes[key])
2195
+ const values = Object.keys(attributes).map(key => attributes[key])
2227
2196
  if (!Array.isArray(attrs)) attrs = [attrs]
2228
2197
  let chunked = chunkArray(attrs, values.length)
2229
- chunked = chunked.filter((val) => {
2198
+ chunked = chunked.filter(val => {
2230
2199
  for (let i = 0; i < val.length; ++i) {
2231
2200
  // the attribute could be a boolean
2232
2201
  if (typeof val[i] === 'boolean') return val[i] === values[i]
@@ -2235,10 +2204,7 @@ class Playwright extends Helper {
2235
2204
  }
2236
2205
  return true
2237
2206
  })
2238
- return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
2239
- chunked.length,
2240
- elemAmount,
2241
- )
2207
+ return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
2242
2208
  }
2243
2209
 
2244
2210
  /**
@@ -2311,7 +2277,7 @@ class Playwright extends Helper {
2311
2277
  const fullPageOption = fullPage || this.options.fullPageScreenshots
2312
2278
  let outputFile = screenshotOutputFolder(fileName)
2313
2279
 
2314
- this.debug(`Screenshot is saving to ${outputFile}`)
2280
+ this.debugSection('Screenshot', relativeDir(outputFile))
2315
2281
 
2316
2282
  await this.page.screenshot({
2317
2283
  path: outputFile,
@@ -2324,7 +2290,7 @@ class Playwright extends Helper {
2324
2290
  const activeSessionPage = this.sessionPages[sessionName]
2325
2291
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2326
2292
 
2327
- this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2293
+ this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2328
2294
 
2329
2295
  if (activeSessionPage) {
2330
2296
  await activeSessionPage.screenshot({
@@ -2358,9 +2324,7 @@ class Playwright extends Helper {
2358
2324
  method = method.toLowerCase()
2359
2325
  const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
2360
2326
  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
- )
2327
+ throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
2364
2328
  }
2365
2329
 
2366
2330
  if (url.startsWith('/')) {
@@ -2397,10 +2361,7 @@ class Playwright extends Helper {
2397
2361
  if (this.options.recordVideo && this.page && this.page.video()) {
2398
2362
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2399
2363
  for (const sessionName in this.sessionPages) {
2400
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2401
- this.sessionPages[sessionName],
2402
- `${test.title}_${sessionName}.failed`,
2403
- )
2364
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
2404
2365
  }
2405
2366
  }
2406
2367
 
@@ -2408,10 +2369,7 @@ class Playwright extends Helper {
2408
2369
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2409
2370
  for (const sessionName in this.sessionPages) {
2410
2371
  if (!this.sessionPages[sessionName].context) continue
2411
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2412
- this.sessionPages[sessionName].context,
2413
- `${test.title}_${sessionName}.failed`,
2414
- )
2372
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
2415
2373
  }
2416
2374
  }
2417
2375
 
@@ -2425,16 +2383,13 @@ class Playwright extends Helper {
2425
2383
  if (this.options.keepVideoForPassedTests) {
2426
2384
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2427
2385
  for (const sessionName of Object.keys(this.sessionPages)) {
2428
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2429
- this.sessionPages[sessionName],
2430
- `${test.title}_${sessionName}.passed`,
2431
- )
2386
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
2432
2387
  }
2433
2388
  } else {
2434
2389
  this.page
2435
2390
  .video()
2436
2391
  .delete()
2437
- .catch((e) => {})
2392
+ .catch(e => {})
2438
2393
  }
2439
2394
  }
2440
2395
 
@@ -2444,10 +2399,7 @@ class Playwright extends Helper {
2444
2399
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2445
2400
  for (const sessionName in this.sessionPages) {
2446
2401
  if (!this.sessionPages[sessionName].context) continue
2447
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2448
- this.sessionPages[sessionName].context,
2449
- `${test.title}_${sessionName}.passed`,
2450
- )
2402
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
2451
2403
  }
2452
2404
  }
2453
2405
  } else {
@@ -2464,7 +2416,7 @@ class Playwright extends Helper {
2464
2416
  * {{> wait }}
2465
2417
  */
2466
2418
  async wait(sec) {
2467
- return new Promise((done) => {
2419
+ return new Promise(done => {
2468
2420
  setTimeout(done, sec * 1000)
2469
2421
  })
2470
2422
  }
@@ -2480,20 +2432,18 @@ class Playwright extends Helper {
2480
2432
  const context = await this._getContext()
2481
2433
  if (!locator.isXPath()) {
2482
2434
  const valueFn = function ([locator]) {
2483
- return Array.from(document.querySelectorAll(locator)).filter((el) => !el.disabled).length > 0
2435
+ return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0
2484
2436
  }
2485
2437
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2486
2438
  } else {
2487
2439
  const enabledFn = function ([locator, $XPath]) {
2488
- eval($XPath) // eslint-disable-line no-eval
2489
- return $XPath(null, locator).filter((el) => !el.disabled).length > 0
2440
+ eval($XPath)
2441
+ return $XPath(null, locator).filter(el => !el.disabled).length > 0
2490
2442
  }
2491
2443
  waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2492
2444
  }
2493
- return waiter.catch((err) => {
2494
- throw new Error(
2495
- `element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2496
- )
2445
+ return waiter.catch(err => {
2446
+ throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2497
2447
  })
2498
2448
  }
2499
2449
 
@@ -2508,20 +2458,18 @@ class Playwright extends Helper {
2508
2458
  const context = await this._getContext()
2509
2459
  if (!locator.isXPath()) {
2510
2460
  const valueFn = function ([locator]) {
2511
- return Array.from(document.querySelectorAll(locator)).filter((el) => el.disabled).length > 0
2461
+ return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0
2512
2462
  }
2513
2463
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2514
2464
  } else {
2515
2465
  const disabledFn = function ([locator, $XPath]) {
2516
- eval($XPath) // eslint-disable-line no-eval
2517
- return $XPath(null, locator).filter((el) => el.disabled).length > 0
2466
+ eval($XPath)
2467
+ return $XPath(null, locator).filter(el => el.disabled).length > 0
2518
2468
  }
2519
2469
  waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2520
2470
  }
2521
- return waiter.catch((err) => {
2522
- throw new Error(
2523
- `element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2524
- )
2471
+ return waiter.catch(err => {
2472
+ throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2525
2473
  })
2526
2474
  }
2527
2475
 
@@ -2536,26 +2484,21 @@ class Playwright extends Helper {
2536
2484
  const context = await this._getContext()
2537
2485
  if (!locator.isXPath()) {
2538
2486
  const valueFn = function ([locator, value]) {
2539
- return (
2540
- Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
2541
- 0
2542
- )
2487
+ return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2543
2488
  }
2544
2489
  waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
2545
2490
  } else {
2546
2491
  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
2492
+ eval($XPath)
2493
+ return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2549
2494
  }
2550
2495
  waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
2551
2496
  timeout: waitTimeout,
2552
2497
  })
2553
2498
  }
2554
- return waiter.catch((err) => {
2499
+ return waiter.catch(err => {
2555
2500
  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
- )
2501
+ 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
2502
  })
2560
2503
  }
2561
2504
 
@@ -2575,22 +2518,20 @@ class Playwright extends Helper {
2575
2518
  if (!els || els.length === 0) {
2576
2519
  return false
2577
2520
  }
2578
- return Array.prototype.filter.call(els, (el) => el.offsetParent !== null).length === num
2521
+ return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
2579
2522
  }
2580
2523
  waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
2581
2524
  } else {
2582
2525
  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
2526
+ eval($XPath)
2527
+ return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
2585
2528
  }
2586
2529
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
2587
2530
  timeout: waitTimeout,
2588
2531
  })
2589
2532
  }
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
- )
2533
+ return waiter.catch(err => {
2534
+ throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
2594
2535
  })
2595
2536
  }
2596
2537
 
@@ -2598,9 +2539,7 @@ class Playwright extends Helper {
2598
2539
  * {{> waitForClickable }}
2599
2540
  */
2600
2541
  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
- )
2542
+ console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
2604
2543
  console.log('Remove usage of this function')
2605
2544
  }
2606
2545
 
@@ -2616,9 +2555,7 @@ class Playwright extends Helper {
2616
2555
  try {
2617
2556
  await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2618
2557
  } catch (e) {
2619
- throw new Error(
2620
- `element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
2621
- )
2558
+ throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2622
2559
  }
2623
2560
  }
2624
2561
 
@@ -2710,10 +2647,8 @@ class Playwright extends Helper {
2710
2647
  .locator(buildLocatorString(locator))
2711
2648
  .first()
2712
2649
  .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
- )
2650
+ .catch(err => {
2651
+ throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
2717
2652
  })
2718
2653
  }
2719
2654
 
@@ -2739,6 +2674,9 @@ class Playwright extends Helper {
2739
2674
  if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
2740
2675
  return this.context
2741
2676
  }
2677
+ if (this.frame) {
2678
+ return this.frame
2679
+ }
2742
2680
  return this.page
2743
2681
  }
2744
2682
 
@@ -2750,14 +2688,14 @@ class Playwright extends Helper {
2750
2688
 
2751
2689
  return this.page
2752
2690
  .waitForFunction(
2753
- (urlPart) => {
2691
+ urlPart => {
2754
2692
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2755
2693
  return currUrl.indexOf(urlPart) > -1
2756
2694
  },
2757
2695
  urlPart,
2758
2696
  { timeout: waitTimeout },
2759
2697
  )
2760
- .catch(async (e) => {
2698
+ .catch(async e => {
2761
2699
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2762
2700
  if (/Timeout/i.test(e.message)) {
2763
2701
  throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
@@ -2780,14 +2718,14 @@ class Playwright extends Helper {
2780
2718
 
2781
2719
  return this.page
2782
2720
  .waitForFunction(
2783
- (urlPart) => {
2721
+ urlPart => {
2784
2722
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2785
2723
  return currUrl.indexOf(urlPart) > -1
2786
2724
  },
2787
2725
  urlPart,
2788
2726
  { timeout: waitTimeout },
2789
2727
  )
2790
- .catch(async (e) => {
2728
+ .catch(async e => {
2791
2729
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2792
2730
  if (/Timeout/i.test(e.message)) {
2793
2731
  throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
@@ -2803,28 +2741,23 @@ class Playwright extends Helper {
2803
2741
  async waitForText(text, sec = null, context = null) {
2804
2742
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2805
2743
  const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
2806
- let waiter
2807
2744
 
2808
2745
  const contextObject = await this._getContext()
2809
2746
 
2810
2747
  if (context) {
2811
2748
  const locator = new Locator(context, 'css')
2812
- if (!locator.isXPath()) {
2813
- try {
2814
- await contextObject
2749
+ try {
2750
+ if (!locator.isXPath()) {
2751
+ return contextObject
2815
2752
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2816
2753
  .first()
2817
2754
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2818
- } catch (e) {
2819
- throw new Error(`${errorMessage}\n${e.message}`)
2820
2755
  }
2821
- }
2822
2756
 
2823
- if (locator.isXPath()) {
2824
- try {
2825
- await contextObject.waitForFunction(
2757
+ if (locator.isXPath()) {
2758
+ return contextObject.waitForFunction(
2826
2759
  ([locator, text, $XPath]) => {
2827
- eval($XPath) // eslint-disable-line no-eval
2760
+ eval($XPath)
2828
2761
  const el = $XPath(null, locator)
2829
2762
  if (!el.length) return false
2830
2763
  return el[0].innerText.indexOf(text) > -1
@@ -2832,27 +2765,34 @@ class Playwright extends Helper {
2832
2765
  [locator.value, text, $XPath.toString()],
2833
2766
  { timeout: waitTimeout },
2834
2767
  )
2835
- } catch (e) {
2836
- throw new Error(`${errorMessage}\n${e.message}`)
2837
2768
  }
2769
+ } catch (e) {
2770
+ throw new Error(`${errorMessage}\n${e.message}`)
2838
2771
  }
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
2772
  }
2773
+
2774
+ const timeoutGap = waitTimeout + 1000
2775
+
2776
+ // We add basic timeout to make sure we don't wait forever
2777
+ // We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
2778
+ // or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
2779
+ // 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
2780
+ return Promise.race([
2781
+ new Promise((_, reject) => {
2782
+ setTimeout(() => reject(errorMessage), waitTimeout)
2783
+ }),
2784
+ this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2785
+ promiseRetry(
2786
+ async retry => {
2787
+ const textPresent = await contextObject
2788
+ .locator(`:has-text(${JSON.stringify(text)})`)
2789
+ .first()
2790
+ .isVisible()
2791
+ if (!textPresent) retry(errorMessage)
2792
+ },
2793
+ { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
2794
+ ),
2795
+ ])
2856
2796
  }
2857
2797
 
2858
2798
  /**
@@ -3021,11 +2961,11 @@ class Playwright extends Helper {
3021
2961
  }
3022
2962
  } else {
3023
2963
  const visibleFn = function ([locator, $XPath]) {
3024
- eval($XPath) // eslint-disable-line no-eval
2964
+ eval($XPath)
3025
2965
  return $XPath(null, locator).length === 0
3026
2966
  }
3027
2967
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
3028
- return waiter.catch((err) => {
2968
+ return waiter.catch(err => {
3029
2969
  throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
3030
2970
  })
3031
2971
  }
@@ -3047,9 +2987,9 @@ class Playwright extends Helper {
3047
2987
 
3048
2988
  return promiseRetry(
3049
2989
  async (retry, number) => {
3050
- const _grabCookie = async (name) => {
2990
+ const _grabCookie = async name => {
3051
2991
  const cookies = await this.browserContext.cookies()
3052
- const cookie = cookies.filter((c) => c.name === name)
2992
+ const cookie = cookies.filter(c => c.name === name)
3053
2993
  if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
3054
2994
  }
3055
2995
 
@@ -3128,7 +3068,7 @@ class Playwright extends Helper {
3128
3068
  this.recording = true
3129
3069
  this.recordedAtLeastOnce = true
3130
3070
 
3131
- this.page.on('requestfinished', async (request) => {
3071
+ this.page.on('requestfinished', async request => {
3132
3072
  const information = {
3133
3073
  url: request.url(),
3134
3074
  method: request.method(),
@@ -3167,20 +3107,20 @@ class Playwright extends Helper {
3167
3107
  */
3168
3108
  blockTraffic(urls) {
3169
3109
  if (Array.isArray(urls)) {
3170
- urls.forEach((url) => {
3171
- this.page.route(url, (route) => {
3110
+ urls.forEach(url => {
3111
+ this.page.route(url, route => {
3172
3112
  route
3173
3113
  .abort()
3174
3114
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3175
- .catch((e) => {})
3115
+ .catch(e => {})
3176
3116
  })
3177
3117
  })
3178
3118
  } else {
3179
- this.page.route(urls, (route) => {
3119
+ this.page.route(urls, route => {
3180
3120
  route
3181
3121
  .abort()
3182
3122
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3183
- .catch((e) => {})
3123
+ .catch(e => {})
3184
3124
  })
3185
3125
  }
3186
3126
  }
@@ -3209,8 +3149,8 @@ class Playwright extends Helper {
3209
3149
  urls = [urls]
3210
3150
  }
3211
3151
 
3212
- urls.forEach((url) => {
3213
- this.page.route(url, (route) => {
3152
+ urls.forEach(url => {
3153
+ this.page.route(url, route => {
3214
3154
  if (this.page.isClosed()) {
3215
3155
  // Sometimes it happens that browser has been closed in the meantime.
3216
3156
  // In this case we just don't fulfill to prevent error in test scenario.
@@ -3256,13 +3196,10 @@ class Playwright extends Helper {
3256
3196
  */
3257
3197
  grabTrafficUrl(urlMatch) {
3258
3198
  if (!this.recordedAtLeastOnce) {
3259
- throw new Error(
3260
- 'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
3261
- )
3199
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
3262
3200
  }
3263
3201
 
3264
3202
  for (const i in this.requests) {
3265
- // eslint-disable-next-line no-prototype-builtins
3266
3203
  if (this.requests.hasOwnProperty(i)) {
3267
3204
  const request = this.requests[i]
3268
3205
 
@@ -3312,15 +3249,15 @@ class Playwright extends Helper {
3312
3249
  await this.cdpSession.send('Network.enable')
3313
3250
  await this.cdpSession.send('Page.enable')
3314
3251
 
3315
- this.cdpSession.on('Network.webSocketFrameReceived', (payload) => {
3252
+ this.cdpSession.on('Network.webSocketFrameReceived', payload => {
3316
3253
  this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
3317
3254
  })
3318
3255
 
3319
- this.cdpSession.on('Network.webSocketFrameSent', (payload) => {
3256
+ this.cdpSession.on('Network.webSocketFrameSent', payload => {
3320
3257
  this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
3321
3258
  })
3322
3259
 
3323
- this.cdpSession.on('Network.webSocketFrameError', (payload) => {
3260
+ this.cdpSession.on('Network.webSocketFrameError', payload => {
3324
3261
  this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
3325
3262
  })
3326
3263
  }
@@ -3344,9 +3281,7 @@ class Playwright extends Helper {
3344
3281
  grabWebSocketMessages() {
3345
3282
  if (!this.recordingWebSocketMessages) {
3346
3283
  if (!this.recordedWebSocketMessagesAtLeastOnce) {
3347
- throw new Error(
3348
- 'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
3349
- )
3284
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
3350
3285
  }
3351
3286
  }
3352
3287
  return this.webSocketMessages
@@ -3493,17 +3428,13 @@ async function proceedClick(locator, context = null, options = {}) {
3493
3428
  }
3494
3429
  const els = await findClickable.call(this, matcher, locator)
3495
3430
  if (context) {
3496
- assertElementExists(
3497
- els,
3498
- locator,
3499
- 'Clickable element',
3500
- `was not found inside element ${new Locator(context).toString()}`,
3501
- )
3431
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
3502
3432
  } else {
3503
3433
  assertElementExists(els, locator, 'Clickable element')
3504
3434
  }
3505
3435
 
3506
3436
  await highlightActiveElement.call(this, els[0])
3437
+ if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
3507
3438
 
3508
3439
  /*
3509
3440
  using the force true options itself but instead dispatching a click
@@ -3565,16 +3496,13 @@ async function proceedSee(assertType, text, context, strict = false) {
3565
3496
  description = `element ${locator.toString()}`
3566
3497
  const els = await this._locate(locator)
3567
3498
  assertElementExists(els, locator.toString())
3568
- allText = await Promise.all(els.map((el) => el.innerText()))
3499
+ allText = await Promise.all(els.map(el => el.innerText()))
3569
3500
  }
3570
3501
 
3571
3502
  if (strict) {
3572
- return allText.map((elText) => equals(description)[assertType](text, elText))
3503
+ return allText.map(elText => equals(description)[assertType](text, elText))
3573
3504
  }
3574
- return stringIncludes(description)[assertType](
3575
- normalizeSpacesInString(text),
3576
- normalizeSpacesInString(allText.join(' | ')),
3577
- )
3505
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
3578
3506
  }
3579
3507
 
3580
3508
  async function findCheckable(locator, context) {
@@ -3604,7 +3532,7 @@ async function findCheckable(locator, context) {
3604
3532
  async function proceedIsChecked(assertType, option) {
3605
3533
  let els = await findCheckable.call(this, option)
3606
3534
  assertElementExists(els, option, 'Checkable')
3607
- els = await Promise.all(els.map((el) => el.isChecked()))
3535
+ els = await Promise.all(els.map(el => el.isChecked()))
3608
3536
  const selected = els.reduce((prev, cur) => prev || cur)
3609
3537
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3610
3538
  }
@@ -3636,10 +3564,10 @@ async function proceedSeeInField(assertType, field, value) {
3636
3564
  const els = await findFields.call(this, field)
3637
3565
  assertElementExists(els, field, 'Field')
3638
3566
  const el = els[0]
3639
- const tag = await el.evaluate((e) => e.tagName)
3567
+ const tag = await el.evaluate(e => e.tagName)
3640
3568
  const fieldType = await el.getAttribute('type')
3641
3569
 
3642
- const proceedMultiple = async (elements) => {
3570
+ const proceedMultiple = async elements => {
3643
3571
  const fields = Array.isArray(elements) ? elements : [elements]
3644
3572
 
3645
3573
  const elementValues = []
@@ -3653,7 +3581,7 @@ async function proceedSeeInField(assertType, field, value) {
3653
3581
  if (assertType === 'assert') {
3654
3582
  equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
3655
3583
  }
3656
- elementValues.forEach((val) => stringIncludes(`fields by ${field}`)[assertType](value, val))
3584
+ elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
3657
3585
  }
3658
3586
  }
3659
3587
 
@@ -3740,6 +3668,8 @@ function isFrameLocator(locator) {
3740
3668
  }
3741
3669
 
3742
3670
  function assertElementExists(res, locator, prefix, suffix) {
3671
+ // if element text is an empty string, just exit this check
3672
+ if (typeof res === 'string' && res === '') return
3743
3673
  if (!res || res.length === 0) {
3744
3674
  throw new ElementNotFound(locator, prefix, suffix)
3745
3675
  }
@@ -3776,12 +3706,9 @@ async function targetCreatedHandler(page) {
3776
3706
  this.contextLocator = null
3777
3707
  })
3778
3708
  })
3779
- page.on('console', (msg) => {
3709
+ page.on('console', msg => {
3780
3710
  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
- )
3711
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
3785
3712
  }
3786
3713
  consoleLogStore.add(msg)
3787
3714
  })
@@ -3889,7 +3816,7 @@ async function refreshContextSession() {
3889
3816
  const contexts = await this.browser.contexts()
3890
3817
  contexts.shift()
3891
3818
 
3892
- await Promise.all(contexts.map((c) => c.close()))
3819
+ await Promise.all(contexts.map(c => c.close()))
3893
3820
  } catch (e) {
3894
3821
  console.log(e)
3895
3822
  }
@@ -3908,10 +3835,10 @@ async function refreshContextSession() {
3908
3835
  const currentUrl = await this.grabCurrentUrl()
3909
3836
 
3910
3837
  if (currentUrl.startsWith('http')) {
3911
- await this.executeScript('localStorage.clear();').catch((err) => {
3838
+ await this.executeScript('localStorage.clear();').catch(err => {
3912
3839
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3913
3840
  })
3914
- await this.executeScript('sessionStorage.clear();').catch((err) => {
3841
+ await this.executeScript('sessionStorage.clear();').catch(err => {
3915
3842
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3916
3843
  })
3917
3844
  }
@@ -3941,11 +3868,22 @@ async function saveTraceForContext(context, name) {
3941
3868
  }
3942
3869
 
3943
3870
  async function highlightActiveElement(element) {
3944
- if (this.options.highlightElement && global.debugMode) {
3945
- await element.evaluate((el) => {
3871
+ if ((this.options.highlightElement || store.onPause) && store.debugMode) {
3872
+ await element.evaluate(el => {
3946
3873
  const prevStyle = el.style.boxShadow
3947
- el.style.boxShadow = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)'
3874
+ el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
3948
3875
  setTimeout(() => (el.style.boxShadow = prevStyle), 2000)
3949
3876
  })
3950
3877
  }
3951
3878
  }
3879
+
3880
+ async function elToString(el, numberOfElements) {
3881
+ const html = await el.evaluate(node => node.outerHTML)
3882
+ return (
3883
+ html
3884
+ .replace(/\n/g, '')
3885
+ .replace(/\s+/g, ' ')
3886
+ .substring(0, 100 / numberOfElements)
3887
+ .trim() + '...'
3888
+ )
3889
+ }