codeceptjs 4.0.0-beta.4 → 4.0.0-beta.5

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 (150) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +139 -87
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/generate.js +10 -14
  17. package/lib/command/gherkin/snippets.js +75 -73
  18. package/lib/command/gherkin/steps.js +1 -1
  19. package/lib/command/info.js +42 -8
  20. package/lib/command/init.js +13 -12
  21. package/lib/command/interactive.js +10 -2
  22. package/lib/command/list.js +1 -1
  23. package/lib/command/run-multiple/chunk.js +48 -45
  24. package/lib/command/run-multiple.js +12 -35
  25. package/lib/command/run-workers.js +21 -58
  26. package/lib/command/utils.js +5 -6
  27. package/lib/command/workers/runTests.js +262 -220
  28. package/lib/container.js +386 -238
  29. package/lib/data/context.js +10 -13
  30. package/lib/data/dataScenarioConfig.js +8 -8
  31. package/lib/data/dataTableArgument.js +6 -6
  32. package/lib/data/table.js +5 -11
  33. package/lib/effects.js +223 -0
  34. package/lib/element/WebElement.js +327 -0
  35. package/lib/els.js +158 -0
  36. package/lib/event.js +21 -17
  37. package/lib/heal.js +88 -80
  38. package/lib/helper/AI.js +2 -1
  39. package/lib/helper/ApiDataFactory.js +3 -6
  40. package/lib/helper/Appium.js +47 -51
  41. package/lib/helper/FileSystem.js +3 -3
  42. package/lib/helper/GraphQLDataFactory.js +3 -3
  43. package/lib/helper/JSONResponse.js +75 -37
  44. package/lib/helper/Mochawesome.js +31 -9
  45. package/lib/helper/Nightmare.js +35 -53
  46. package/lib/helper/Playwright.js +262 -267
  47. package/lib/helper/Protractor.js +54 -77
  48. package/lib/helper/Puppeteer.js +246 -260
  49. package/lib/helper/REST.js +5 -17
  50. package/lib/helper/TestCafe.js +21 -44
  51. package/lib/helper/WebDriver.js +151 -170
  52. package/lib/helper/extras/Popup.js +22 -22
  53. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  54. package/lib/listener/emptyRun.js +55 -0
  55. package/lib/listener/exit.js +7 -10
  56. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  57. package/lib/listener/globalTimeout.js +165 -0
  58. package/lib/listener/helpers.js +15 -15
  59. package/lib/listener/mocha.js +1 -1
  60. package/lib/listener/result.js +12 -0
  61. package/lib/listener/retryEnhancer.js +85 -0
  62. package/lib/listener/steps.js +32 -18
  63. package/lib/listener/store.js +20 -0
  64. package/lib/mocha/asyncWrapper.js +231 -0
  65. package/lib/{interfaces → mocha}/bdd.js +3 -3
  66. package/lib/mocha/cli.js +308 -0
  67. package/lib/mocha/factory.js +104 -0
  68. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  69. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  70. package/lib/mocha/hooks.js +112 -0
  71. package/lib/mocha/index.js +12 -0
  72. package/lib/mocha/inject.js +29 -0
  73. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  74. package/lib/mocha/suite.js +82 -0
  75. package/lib/mocha/test.js +181 -0
  76. package/lib/mocha/types.d.ts +42 -0
  77. package/lib/mocha/ui.js +232 -0
  78. package/lib/output.js +82 -62
  79. package/lib/pause.js +160 -138
  80. package/lib/plugin/analyze.js +396 -0
  81. package/lib/plugin/auth.js +435 -0
  82. package/lib/plugin/autoDelay.js +8 -8
  83. package/lib/plugin/autoLogin.js +3 -338
  84. package/lib/plugin/commentStep.js +6 -1
  85. package/lib/plugin/coverage.js +10 -19
  86. package/lib/plugin/customLocator.js +3 -3
  87. package/lib/plugin/customReporter.js +52 -0
  88. package/lib/plugin/eachElement.js +1 -1
  89. package/lib/plugin/fakerTransform.js +1 -1
  90. package/lib/plugin/heal.js +36 -9
  91. package/lib/plugin/htmlReporter.js +1947 -0
  92. package/lib/plugin/pageInfo.js +140 -0
  93. package/lib/plugin/retryFailedStep.js +17 -18
  94. package/lib/plugin/retryTo.js +2 -113
  95. package/lib/plugin/screenshotOnFail.js +17 -58
  96. package/lib/plugin/selenoid.js +15 -35
  97. package/lib/plugin/standardActingHelpers.js +4 -1
  98. package/lib/plugin/stepByStepReport.js +56 -17
  99. package/lib/plugin/stepTimeout.js +5 -12
  100. package/lib/plugin/subtitles.js +4 -4
  101. package/lib/plugin/tryTo.js +3 -102
  102. package/lib/plugin/wdio.js +8 -10
  103. package/lib/recorder.js +155 -124
  104. package/lib/rerun.js +43 -42
  105. package/lib/result.js +161 -0
  106. package/lib/secret.js +1 -1
  107. package/lib/step/base.js +239 -0
  108. package/lib/step/comment.js +10 -0
  109. package/lib/step/config.js +50 -0
  110. package/lib/step/func.js +46 -0
  111. package/lib/step/helper.js +50 -0
  112. package/lib/step/meta.js +99 -0
  113. package/lib/step/record.js +74 -0
  114. package/lib/step/retry.js +11 -0
  115. package/lib/step/section.js +55 -0
  116. package/lib/step.js +21 -332
  117. package/lib/steps.js +50 -0
  118. package/lib/store.js +37 -5
  119. package/lib/template/heal.js +2 -11
  120. package/lib/test-server.js +323 -0
  121. package/lib/timeout.js +66 -0
  122. package/lib/utils.js +351 -218
  123. package/lib/within.js +75 -55
  124. package/lib/workerStorage.js +2 -1
  125. package/lib/workers.js +386 -276
  126. package/package.json +76 -70
  127. package/translations/de-DE.js +4 -3
  128. package/translations/fr-FR.js +4 -3
  129. package/translations/index.js +1 -0
  130. package/translations/it-IT.js +4 -3
  131. package/translations/ja-JP.js +4 -3
  132. package/translations/nl-NL.js +76 -0
  133. package/translations/pl-PL.js +4 -3
  134. package/translations/pt-BR.js +4 -3
  135. package/translations/ru-RU.js +4 -3
  136. package/translations/utils.js +9 -0
  137. package/translations/zh-CN.js +4 -3
  138. package/translations/zh-TW.js +4 -3
  139. package/typings/index.d.ts +188 -186
  140. package/typings/promiseBasedTypes.d.ts +18 -705
  141. package/typings/types.d.ts +301 -804
  142. package/lib/cli.js +0 -256
  143. package/lib/helper/ExpectHelper.js +0 -391
  144. package/lib/helper/SoftExpectHelper.js +0 -381
  145. package/lib/listener/artifacts.js +0 -19
  146. package/lib/listener/timeout.js +0 -109
  147. package/lib/mochaFactory.js +0 -113
  148. package/lib/plugin/debugErrors.js +0 -67
  149. package/lib/scenario.js +0 -224
  150. 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')
@@ -31,6 +33,7 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
31
33
  const Popup = require('./extras/Popup')
32
34
  const Console = require('./extras/Console')
33
35
  const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator')
36
+ const WebElement = require('../element/WebElement')
34
37
 
35
38
  let playwright
36
39
  let perfTiming
@@ -40,26 +43,10 @@ const popupStore = new Popup()
40
43
  const consoleLogStore = new Console()
41
44
  const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
42
45
 
43
- const {
44
- setRestartStrategy,
45
- restartsSession,
46
- restartsContext,
47
- restartsBrowser,
48
- } = require('./extras/PlaywrightRestartOpts')
46
+ const { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts')
49
47
  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')
48
+ const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
49
+ const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
63
50
 
64
51
  const pathSeparator = path.sep
65
52
 
@@ -392,9 +379,7 @@ class Playwright extends Helper {
392
379
  config = Object.assign(defaults, config)
393
380
 
394
381
  if (availableBrowsers.indexOf(config.browser) < 0) {
395
- throw new Error(
396
- `Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`,
397
- )
382
+ throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`)
398
383
  }
399
384
 
400
385
  return config
@@ -440,9 +425,7 @@ class Playwright extends Helper {
440
425
  }
441
426
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
442
427
  this.isElectron = this.options.browser === 'electron'
443
- this.userDataDir = this.playwrightOptions.userDataDir
444
- ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}`
445
- : undefined
428
+ this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined
446
429
  this.isCDPConnection = this.playwrightOptions.cdpConnection
447
430
  popupStore.defaultAction = this.options.defaultPopupAction
448
431
  }
@@ -458,14 +441,14 @@ class Playwright extends Helper {
458
441
  name: 'url',
459
442
  message: 'Base url of site to be tested',
460
443
  default: 'http://localhost',
461
- when: (answers) => answers.Playwright_browser !== 'electron',
444
+ when: answers => answers.Playwright_browser !== 'electron',
462
445
  },
463
446
  {
464
447
  name: 'show',
465
448
  message: 'Show browser window',
466
449
  default: true,
467
450
  type: 'confirm',
468
- when: (answers) => answers.Playwright_browser !== 'electron',
451
+ when: answers => answers.Playwright_browser !== 'electron',
469
452
  },
470
453
  ]
471
454
  }
@@ -500,9 +483,10 @@ class Playwright extends Helper {
500
483
 
501
484
  async _before(test) {
502
485
  this.currentRunningTest = test
486
+
503
487
  recorder.retry({
504
- retries: process.env.FAILED_STEP_RETRIES || 3,
505
- when: (err) => {
488
+ retries: test?.opts?.conditionalRetries || 3,
489
+ when: err => {
506
490
  if (!err || typeof err.message !== 'string') {
507
491
  return false
508
492
  }
@@ -540,12 +524,17 @@ class Playwright extends Helper {
540
524
  this.currentRunningTest.artifacts.har = fileName
541
525
  contextOptions.recordHar = this.options.recordHar
542
526
  }
527
+
528
+ // load pre-saved cookies
529
+ if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
530
+
543
531
  if (this.storageState) contextOptions.storageState = this.storageState
544
532
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
545
533
  if (this.options.locale) contextOptions.locale = this.options.locale
546
534
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
547
535
  this.contextOptions = contextOptions
548
536
  if (!this.browserContext || !restartsSession()) {
537
+ this.debugSection('New Session', JSON.stringify(this.contextOptions))
549
538
  this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
550
539
  }
551
540
  }
@@ -559,10 +548,7 @@ class Playwright extends Helper {
559
548
  mainPage = existingPages[0] || (await this.browserContext.newPage())
560
549
  } catch (e) {
561
550
  if (this.playwrightOptions.userDataDir) {
562
- this.browser = await playwright[this.options.browser].launchPersistentContext(
563
- this.userDataDir,
564
- this.playwrightOptions,
565
- )
551
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
566
552
  this.browserContext = this.browser
567
553
  const existingPages = await this.browserContext.pages()
568
554
  mainPage = existingPages[0]
@@ -573,6 +559,15 @@ class Playwright extends Helper {
573
559
 
574
560
  await this._setPage(mainPage)
575
561
 
562
+ try {
563
+ // set metadata for reporting
564
+ test.meta.browser = this.browser.browserType().name()
565
+ test.meta.browserVersion = this.browser.version()
566
+ test.meta.windowSize = `${this.page.viewportSize().width}x${this.page.viewportSize().height}`
567
+ } catch (e) {
568
+ this.debug('Failed to set metadata for reporting')
569
+ }
570
+
576
571
  if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true })
577
572
 
578
573
  return this.browser
@@ -583,7 +578,7 @@ class Playwright extends Helper {
583
578
 
584
579
  if (this.isElectron) {
585
580
  this.browser.close()
586
- this.electronSessions.forEach((session) => session.close())
581
+ this.electronSessions.forEach(session => session.close())
587
582
  return
588
583
  }
589
584
 
@@ -605,7 +600,7 @@ class Playwright extends Helper {
605
600
  this.storageState = await currentContext.storageState()
606
601
  }
607
602
 
608
- await Promise.all(contexts.map((c) => c.close()))
603
+ await Promise.all(contexts.map(c => c.close()))
609
604
  }
610
605
  } catch (e) {
611
606
  console.log(e)
@@ -641,10 +636,7 @@ class Playwright extends Helper {
641
636
  page = await browserContext.newPage()
642
637
  } catch (e) {
643
638
  if (this.playwrightOptions.userDataDir) {
644
- browserContext = await playwright[this.options.browser].launchPersistentContext(
645
- `${this.userDataDir}_${this.activeSessionName}`,
646
- this.playwrightOptions,
647
- )
639
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
648
640
  this.browser = browserContext
649
641
  page = await browserContext.pages()[0]
650
642
  }
@@ -660,7 +652,7 @@ class Playwright extends Helper {
660
652
  stop: async () => {
661
653
  // is closed by _after
662
654
  },
663
- loadVars: async (context) => {
655
+ loadVars: async context => {
664
656
  if (context) {
665
657
  this.browserContext = context
666
658
  const existingPages = await context.pages()
@@ -668,7 +660,7 @@ class Playwright extends Helper {
668
660
  return this._setPage(this.sessionPages[this.activeSessionName])
669
661
  }
670
662
  },
671
- restoreVars: async (session) => {
663
+ restoreVars: async session => {
672
664
  this.withinLocator = null
673
665
  this.browserContext = defaultContext
674
666
 
@@ -793,7 +785,7 @@ class Playwright extends Helper {
793
785
  return
794
786
  }
795
787
  page.removeAllListeners('dialog')
796
- page.on('dialog', async (dialog) => {
788
+ page.on('dialog', async dialog => {
797
789
  popupStore.popup = dialog
798
790
  const action = popupStore.actionType || this.options.defaultPopupAction
799
791
  await this._waitForAction()
@@ -856,16 +848,13 @@ class Playwright extends Helper {
856
848
  throw err
857
849
  }
858
850
  } else if (this.playwrightOptions.userDataDir) {
859
- this.browser = await playwright[this.options.browser].launchPersistentContext(
860
- this.userDataDir,
861
- this.playwrightOptions,
862
- )
851
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
863
852
  } else {
864
853
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions)
865
854
  }
866
855
 
867
856
  // works only for Chromium
868
- this.browser.on('targetchanged', (target) => {
857
+ this.browser.on('targetchanged', target => {
869
858
  this.debugSection('Url', target.url())
870
859
  })
871
860
 
@@ -940,7 +929,7 @@ class Playwright extends Helper {
940
929
  const navigationStart = timing.navigationStart
941
930
 
942
931
  const extractedData = {}
943
- dataNames.forEach((name) => {
932
+ dataNames.forEach(name => {
944
933
  extractedData[name] = timing[name] - navigationStart
945
934
  })
946
935
 
@@ -955,7 +944,8 @@ class Playwright extends Helper {
955
944
  throw new Error('Cannot open pages inside an Electron container')
956
945
  }
957
946
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
958
- url = this.options.url + (url.startsWith('/') ? url : `/${url}`)
947
+ url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
948
+ this.debug(`Changed URL to base url + relative path: ${url}`)
959
949
  }
960
950
 
961
951
  if (this.options.basicAuth && this.isAuthenticated !== true) {
@@ -969,13 +959,7 @@ class Playwright extends Helper {
969
959
 
970
960
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
971
961
 
972
- perfTiming = this._extractDataFromPerformanceTiming(
973
- performanceTiming,
974
- 'responseEnd',
975
- 'domInteractive',
976
- 'domContentLoadedEventEnd',
977
- 'loadEventEnd',
978
- )
962
+ perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
979
963
 
980
964
  return this._waitForAction()
981
965
  }
@@ -1197,10 +1181,7 @@ class Playwright extends Helper {
1197
1181
  return this.executeScript(() => {
1198
1182
  const body = document.body
1199
1183
  const html = document.documentElement
1200
- window.scrollTo(
1201
- 0,
1202
- Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
1203
- )
1184
+ window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
1204
1185
  })
1205
1186
  }
1206
1187
 
@@ -1287,7 +1268,22 @@ class Playwright extends Helper {
1287
1268
 
1288
1269
  if (this.frame) return findElements(this.frame, locator)
1289
1270
 
1290
- return findElements(context, locator)
1271
+ const els = await findElements(context, locator)
1272
+
1273
+ if (store.debugMode) {
1274
+ const previewElements = els.slice(0, 3)
1275
+ let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))
1276
+ if (els.length > 3) htmls.push('...')
1277
+ if (els.length > 1) {
1278
+ this.debugSection(`Elements (${els.length})`, htmls.join('|').trim())
1279
+ } else if (els.length === 1) {
1280
+ this.debugSection('Element', htmls.join('|').trim())
1281
+ } else {
1282
+ this.debug(`No elements found by ${JSON.stringify(locator).slice(0, 50)}....`)
1283
+ }
1284
+ }
1285
+
1286
+ return els
1291
1287
  }
1292
1288
 
1293
1289
  /**
@@ -1346,7 +1342,8 @@ class Playwright extends Helper {
1346
1342
  *
1347
1343
  */
1348
1344
  async grabWebElements(locator) {
1349
- return this._locate(locator)
1345
+ const elements = await this._locate(locator)
1346
+ return elements.map(element => new WebElement(element, this))
1350
1347
  }
1351
1348
 
1352
1349
  /**
@@ -1354,7 +1351,8 @@ class Playwright extends Helper {
1354
1351
  *
1355
1352
  */
1356
1353
  async grabWebElement(locator) {
1357
- return this._locateElement(locator)
1354
+ const element = await this._locateElement(locator)
1355
+ return new WebElement(element, this)
1358
1356
  }
1359
1357
 
1360
1358
  /**
@@ -1437,10 +1435,10 @@ class Playwright extends Helper {
1437
1435
  */
1438
1436
  async closeOtherTabs() {
1439
1437
  const pages = await this.browserContext.pages()
1440
- const otherPages = pages.filter((page) => page !== this.page)
1438
+ const otherPages = pages.filter(page => page !== this.page)
1441
1439
  if (otherPages.length) {
1442
1440
  this.debug(`Closing ${otherPages.length} tabs`)
1443
- return Promise.all(otherPages.map((p) => p.close()))
1441
+ return Promise.all(otherPages.map(p => p.close()))
1444
1442
  }
1445
1443
  return Promise.resolve()
1446
1444
  }
@@ -1483,9 +1481,9 @@ class Playwright extends Helper {
1483
1481
  */
1484
1482
  async seeElement(locator) {
1485
1483
  let els = await this._locate(locator)
1486
- els = await Promise.all(els.map((el) => el.isVisible()))
1484
+ els = await Promise.all(els.map(el => el.isVisible()))
1487
1485
  try {
1488
- return empty('visible elements').negate(els.filter((v) => v).fill('ELEMENT'))
1486
+ return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
1489
1487
  } catch (e) {
1490
1488
  dontSeeElementError(locator)
1491
1489
  }
@@ -1497,9 +1495,9 @@ class Playwright extends Helper {
1497
1495
  */
1498
1496
  async dontSeeElement(locator) {
1499
1497
  let els = await this._locate(locator)
1500
- els = await Promise.all(els.map((el) => el.isVisible()))
1498
+ els = await Promise.all(els.map(el => el.isVisible()))
1501
1499
  try {
1502
- return empty('visible elements').assert(els.filter((v) => v).fill('ELEMENT'))
1500
+ return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
1503
1501
  } catch (e) {
1504
1502
  seeElementError(locator)
1505
1503
  }
@@ -1511,7 +1509,7 @@ class Playwright extends Helper {
1511
1509
  async seeElementInDOM(locator) {
1512
1510
  const els = await this._locate(locator)
1513
1511
  try {
1514
- return empty('elements on page').negate(els.filter((v) => v).fill('ELEMENT'))
1512
+ return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
1515
1513
  } catch (e) {
1516
1514
  dontSeeElementInDOMError(locator)
1517
1515
  }
@@ -1523,7 +1521,7 @@ class Playwright extends Helper {
1523
1521
  async dontSeeElementInDOM(locator) {
1524
1522
  const els = await this._locate(locator)
1525
1523
  try {
1526
- return empty('elements on a page').assert(els.filter((v) => v).fill('ELEMENT'))
1524
+ return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
1527
1525
  } catch (e) {
1528
1526
  seeElementInDOMError(locator)
1529
1527
  }
@@ -1547,7 +1545,7 @@ class Playwright extends Helper {
1547
1545
  * @return {Promise<void>}
1548
1546
  */
1549
1547
  async handleDownloads(fileName) {
1550
- this.page.waitForEvent('download').then(async (download) => {
1548
+ this.page.waitForEvent('download').then(async download => {
1551
1549
  const filePath = await download.path()
1552
1550
  fileName = fileName || `downloads/${path.basename(filePath)}`
1553
1551
 
@@ -1741,6 +1739,7 @@ class Playwright extends Helper {
1741
1739
  const el = els[0]
1742
1740
 
1743
1741
  await el.clear()
1742
+ if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
1744
1743
 
1745
1744
  await highlightActiveElement.call(this, el)
1746
1745
 
@@ -1852,8 +1851,8 @@ class Playwright extends Helper {
1852
1851
  */
1853
1852
  async grabNumberOfVisibleElements(locator) {
1854
1853
  let els = await this._locate(locator)
1855
- els = await Promise.all(els.map((el) => el.isVisible()))
1856
- return els.filter((v) => v).length
1854
+ els = await Promise.all(els.map(el => el.isVisible()))
1855
+ return els.filter(v => v).length
1857
1856
  }
1858
1857
 
1859
1858
  /**
@@ -1963,9 +1962,7 @@ class Playwright extends Helper {
1963
1962
  */
1964
1963
  async seeNumberOfElements(locator, num) {
1965
1964
  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)
1965
+ return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
1969
1966
  }
1970
1967
 
1971
1968
  /**
@@ -1975,10 +1972,7 @@ class Playwright extends Helper {
1975
1972
  */
1976
1973
  async seeNumberOfVisibleElements(locator, num) {
1977
1974
  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
- )
1975
+ return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
1982
1976
  }
1983
1977
 
1984
1978
  /**
@@ -1997,7 +1991,7 @@ class Playwright extends Helper {
1997
1991
  */
1998
1992
  async seeCookie(name) {
1999
1993
  const cookies = await this.browserContext.cookies()
2000
- empty(`cookie ${name} to be set`).negate(cookies.filter((c) => c.name === name))
1994
+ empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
2001
1995
  }
2002
1996
 
2003
1997
  /**
@@ -2005,7 +1999,7 @@ class Playwright extends Helper {
2005
1999
  */
2006
2000
  async dontSeeCookie(name) {
2007
2001
  const cookies = await this.browserContext.cookies()
2008
- empty(`cookie ${name} not to be set`).assert(cookies.filter((c) => c.name === name))
2002
+ empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
2009
2003
  }
2010
2004
 
2011
2005
  /**
@@ -2016,17 +2010,18 @@ class Playwright extends Helper {
2016
2010
  async grabCookie(name) {
2017
2011
  const cookies = await this.browserContext.cookies()
2018
2012
  if (!name) return cookies
2019
- const cookie = cookies.filter((c) => c.name === name)
2013
+ const cookie = cookies.filter(c => c.name === name)
2020
2014
  if (cookie[0]) return cookie[0]
2021
2015
  }
2022
2016
 
2023
2017
  /**
2024
2018
  * {{> clearCookie }}
2025
2019
  */
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
2020
+ async clearCookie(cookieName) {
2029
2021
  if (!this.browserContext) return
2022
+ if (cookieName) {
2023
+ return this.browserContext.clearCookies({ name: cookieName })
2024
+ }
2030
2025
  return this.browserContext.clearCookies()
2031
2026
  }
2032
2027
 
@@ -2098,7 +2093,7 @@ class Playwright extends Helper {
2098
2093
  const els = await this._locate(locator)
2099
2094
  const texts = []
2100
2095
  for (const el of els) {
2101
- texts.push(await await el.innerText())
2096
+ texts.push(await el.innerText())
2102
2097
  }
2103
2098
  this.debug(`Matched ${els.length} elements`)
2104
2099
  return texts
@@ -2120,7 +2115,7 @@ class Playwright extends Helper {
2120
2115
  async grabValueFromAll(locator) {
2121
2116
  const els = await findFields.call(this, locator)
2122
2117
  this.debug(`Matched ${els.length} elements`)
2123
- return Promise.all(els.map((el) => el.inputValue()))
2118
+ return Promise.all(els.map(el => el.inputValue()))
2124
2119
  }
2125
2120
 
2126
2121
  /**
@@ -2139,7 +2134,7 @@ class Playwright extends Helper {
2139
2134
  async grabHTMLFromAll(locator) {
2140
2135
  const els = await this._locate(locator)
2141
2136
  this.debug(`Matched ${els.length} elements`)
2142
- return Promise.all(els.map((el) => el.innerHTML()))
2137
+ return Promise.all(els.map(el => el.innerHTML()))
2143
2138
  }
2144
2139
 
2145
2140
  /**
@@ -2160,11 +2155,7 @@ class Playwright extends Helper {
2160
2155
  async grabCssPropertyFromAll(locator, cssProperty) {
2161
2156
  const els = await this._locate(locator)
2162
2157
  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
- )
2158
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2168
2159
 
2169
2160
  return cssValues
2170
2161
  }
@@ -2192,19 +2183,16 @@ class Playwright extends Helper {
2192
2183
  }
2193
2184
  }
2194
2185
 
2195
- const values = Object.keys(cssPropertiesCamelCase).map((key) => cssPropertiesCamelCase[key])
2186
+ const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
2196
2187
  if (!Array.isArray(props)) props = [props]
2197
2188
  let chunked = chunkArray(props, values.length)
2198
- chunked = chunked.filter((val) => {
2189
+ chunked = chunked.filter(val => {
2199
2190
  for (let i = 0; i < val.length; ++i) {
2200
- // eslint-disable-next-line eqeqeq
2201
2191
  if (val[i] != values[i]) return false
2202
2192
  }
2203
2193
  return true
2204
2194
  })
2205
- return equals(
2206
- `all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
2207
- ).assert(chunked.length, elemAmount)
2195
+ return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
2208
2196
  }
2209
2197
 
2210
2198
  /**
@@ -2217,16 +2205,16 @@ class Playwright extends Helper {
2217
2205
 
2218
2206
  const elemAmount = res.length
2219
2207
  const commands = []
2220
- res.forEach((el) => {
2221
- Object.keys(attributes).forEach((prop) => {
2208
+ res.forEach(el => {
2209
+ Object.keys(attributes).forEach(prop => {
2222
2210
  commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
2223
2211
  })
2224
2212
  })
2225
2213
  let attrs = await Promise.all(commands)
2226
- const values = Object.keys(attributes).map((key) => attributes[key])
2214
+ const values = Object.keys(attributes).map(key => attributes[key])
2227
2215
  if (!Array.isArray(attrs)) attrs = [attrs]
2228
2216
  let chunked = chunkArray(attrs, values.length)
2229
- chunked = chunked.filter((val) => {
2217
+ chunked = chunked.filter(val => {
2230
2218
  for (let i = 0; i < val.length; ++i) {
2231
2219
  // the attribute could be a boolean
2232
2220
  if (typeof val[i] === 'boolean') return val[i] === values[i]
@@ -2235,10 +2223,7 @@ class Playwright extends Helper {
2235
2223
  }
2236
2224
  return true
2237
2225
  })
2238
- return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
2239
- chunked.length,
2240
- elemAmount,
2241
- )
2226
+ return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
2242
2227
  }
2243
2228
 
2244
2229
  /**
@@ -2311,7 +2296,7 @@ class Playwright extends Helper {
2311
2296
  const fullPageOption = fullPage || this.options.fullPageScreenshots
2312
2297
  let outputFile = screenshotOutputFolder(fileName)
2313
2298
 
2314
- this.debug(`Screenshot is saving to ${outputFile}`)
2299
+ this.debugSection('Screenshot', relativeDir(outputFile))
2315
2300
 
2316
2301
  await this.page.screenshot({
2317
2302
  path: outputFile,
@@ -2324,7 +2309,7 @@ class Playwright extends Helper {
2324
2309
  const activeSessionPage = this.sessionPages[sessionName]
2325
2310
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2326
2311
 
2327
- this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2312
+ this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2328
2313
 
2329
2314
  if (activeSessionPage) {
2330
2315
  await activeSessionPage.screenshot({
@@ -2358,9 +2343,7 @@ class Playwright extends Helper {
2358
2343
  method = method.toLowerCase()
2359
2344
  const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
2360
2345
  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
- )
2346
+ throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
2364
2347
  }
2365
2348
 
2366
2349
  if (url.startsWith('/')) {
@@ -2397,21 +2380,19 @@ class Playwright extends Helper {
2397
2380
  if (this.options.recordVideo && this.page && this.page.video()) {
2398
2381
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2399
2382
  for (const sessionName in this.sessionPages) {
2400
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2401
- this.sessionPages[sessionName],
2402
- `${test.title}_${sessionName}.failed`,
2403
- )
2383
+ if (sessionName === '') continue
2384
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
2404
2385
  }
2405
2386
  }
2406
2387
 
2407
2388
  if (this.options.trace) {
2408
2389
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2409
2390
  for (const sessionName in this.sessionPages) {
2410
- if (!this.sessionPages[sessionName].context) continue
2411
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2412
- this.sessionPages[sessionName].context,
2413
- `${test.title}_${sessionName}.failed`,
2414
- )
2391
+ if (sessionName === '') continue
2392
+ const sessionPage = this.sessionPages[sessionName]
2393
+ const sessionContext = sessionPage.context()
2394
+ if (!sessionContext || !sessionContext.tracing) continue
2395
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.failed`)
2415
2396
  }
2416
2397
  }
2417
2398
 
@@ -2425,16 +2406,14 @@ class Playwright extends Helper {
2425
2406
  if (this.options.keepVideoForPassedTests) {
2426
2407
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2427
2408
  for (const sessionName of Object.keys(this.sessionPages)) {
2428
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2429
- this.sessionPages[sessionName],
2430
- `${test.title}_${sessionName}.passed`,
2431
- )
2409
+ if (sessionName === '') continue
2410
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
2432
2411
  }
2433
2412
  } else {
2434
2413
  this.page
2435
2414
  .video()
2436
2415
  .delete()
2437
- .catch((e) => {})
2416
+ .catch(e => {})
2438
2417
  }
2439
2418
  }
2440
2419
 
@@ -2443,11 +2422,11 @@ class Playwright extends Helper {
2443
2422
  if (this.options.trace) {
2444
2423
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2445
2424
  for (const sessionName in this.sessionPages) {
2446
- if (!this.sessionPages[sessionName].context) continue
2447
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2448
- this.sessionPages[sessionName].context,
2449
- `${test.title}_${sessionName}.passed`,
2450
- )
2425
+ if (sessionName === '') continue
2426
+ const sessionPage = this.sessionPages[sessionName]
2427
+ const sessionContext = sessionPage.context()
2428
+ if (!sessionContext || !sessionContext.tracing) continue
2429
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.passed`)
2451
2430
  }
2452
2431
  }
2453
2432
  } else {
@@ -2464,7 +2443,7 @@ class Playwright extends Helper {
2464
2443
  * {{> wait }}
2465
2444
  */
2466
2445
  async wait(sec) {
2467
- return new Promise((done) => {
2446
+ return new Promise(done => {
2468
2447
  setTimeout(done, sec * 1000)
2469
2448
  })
2470
2449
  }
@@ -2480,20 +2459,18 @@ class Playwright extends Helper {
2480
2459
  const context = await this._getContext()
2481
2460
  if (!locator.isXPath()) {
2482
2461
  const valueFn = function ([locator]) {
2483
- return Array.from(document.querySelectorAll(locator)).filter((el) => !el.disabled).length > 0
2462
+ return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0
2484
2463
  }
2485
2464
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2486
2465
  } else {
2487
2466
  const enabledFn = function ([locator, $XPath]) {
2488
- eval($XPath) // eslint-disable-line no-eval
2489
- return $XPath(null, locator).filter((el) => !el.disabled).length > 0
2467
+ eval($XPath)
2468
+ return $XPath(null, locator).filter(el => !el.disabled).length > 0
2490
2469
  }
2491
2470
  waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2492
2471
  }
2493
- return waiter.catch((err) => {
2494
- throw new Error(
2495
- `element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2496
- )
2472
+ return waiter.catch(err => {
2473
+ throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2497
2474
  })
2498
2475
  }
2499
2476
 
@@ -2508,20 +2485,18 @@ class Playwright extends Helper {
2508
2485
  const context = await this._getContext()
2509
2486
  if (!locator.isXPath()) {
2510
2487
  const valueFn = function ([locator]) {
2511
- return Array.from(document.querySelectorAll(locator)).filter((el) => el.disabled).length > 0
2488
+ return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0
2512
2489
  }
2513
2490
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2514
2491
  } else {
2515
2492
  const disabledFn = function ([locator, $XPath]) {
2516
- eval($XPath) // eslint-disable-line no-eval
2517
- return $XPath(null, locator).filter((el) => el.disabled).length > 0
2493
+ eval($XPath)
2494
+ return $XPath(null, locator).filter(el => el.disabled).length > 0
2518
2495
  }
2519
2496
  waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2520
2497
  }
2521
- return waiter.catch((err) => {
2522
- throw new Error(
2523
- `element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2524
- )
2498
+ return waiter.catch(err => {
2499
+ throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2525
2500
  })
2526
2501
  }
2527
2502
 
@@ -2536,26 +2511,21 @@ class Playwright extends Helper {
2536
2511
  const context = await this._getContext()
2537
2512
  if (!locator.isXPath()) {
2538
2513
  const valueFn = function ([locator, value]) {
2539
- return (
2540
- Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
2541
- 0
2542
- )
2514
+ return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2543
2515
  }
2544
2516
  waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
2545
2517
  } else {
2546
2518
  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
2519
+ eval($XPath)
2520
+ return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2549
2521
  }
2550
2522
  waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
2551
2523
  timeout: waitTimeout,
2552
2524
  })
2553
2525
  }
2554
- return waiter.catch((err) => {
2526
+ return waiter.catch(err => {
2555
2527
  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
- )
2528
+ 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
2529
  })
2560
2530
  }
2561
2531
 
@@ -2575,22 +2545,20 @@ class Playwright extends Helper {
2575
2545
  if (!els || els.length === 0) {
2576
2546
  return false
2577
2547
  }
2578
- return Array.prototype.filter.call(els, (el) => el.offsetParent !== null).length === num
2548
+ return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
2579
2549
  }
2580
2550
  waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
2581
2551
  } else {
2582
2552
  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
2553
+ eval($XPath)
2554
+ return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
2585
2555
  }
2586
2556
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
2587
2557
  timeout: waitTimeout,
2588
2558
  })
2589
2559
  }
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
- )
2560
+ return waiter.catch(err => {
2561
+ throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
2594
2562
  })
2595
2563
  }
2596
2564
 
@@ -2598,9 +2566,7 @@ class Playwright extends Helper {
2598
2566
  * {{> waitForClickable }}
2599
2567
  */
2600
2568
  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
- )
2569
+ console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
2604
2570
  console.log('Remove usage of this function')
2605
2571
  }
2606
2572
 
@@ -2616,9 +2582,7 @@ class Playwright extends Helper {
2616
2582
  try {
2617
2583
  await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2618
2584
  } catch (e) {
2619
- throw new Error(
2620
- `element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
2621
- )
2585
+ throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2622
2586
  }
2623
2587
  }
2624
2588
 
@@ -2710,10 +2674,8 @@ class Playwright extends Helper {
2710
2674
  .locator(buildLocatorString(locator))
2711
2675
  .first()
2712
2676
  .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
- )
2677
+ .catch(err => {
2678
+ throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
2717
2679
  })
2718
2680
  }
2719
2681
 
@@ -2739,6 +2701,9 @@ class Playwright extends Helper {
2739
2701
  if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
2740
2702
  return this.context
2741
2703
  }
2704
+ if (this.frame) {
2705
+ return this.frame
2706
+ }
2742
2707
  return this.page
2743
2708
  }
2744
2709
 
@@ -2750,14 +2715,14 @@ class Playwright extends Helper {
2750
2715
 
2751
2716
  return this.page
2752
2717
  .waitForFunction(
2753
- (urlPart) => {
2718
+ urlPart => {
2754
2719
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2755
2720
  return currUrl.indexOf(urlPart) > -1
2756
2721
  },
2757
2722
  urlPart,
2758
2723
  { timeout: waitTimeout },
2759
2724
  )
2760
- .catch(async (e) => {
2725
+ .catch(async e => {
2761
2726
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2762
2727
  if (/Timeout/i.test(e.message)) {
2763
2728
  throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
@@ -2780,14 +2745,14 @@ class Playwright extends Helper {
2780
2745
 
2781
2746
  return this.page
2782
2747
  .waitForFunction(
2783
- (urlPart) => {
2748
+ urlPart => {
2784
2749
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2785
2750
  return currUrl.indexOf(urlPart) > -1
2786
2751
  },
2787
2752
  urlPart,
2788
2753
  { timeout: waitTimeout },
2789
2754
  )
2790
- .catch(async (e) => {
2755
+ .catch(async e => {
2791
2756
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2792
2757
  if (/Timeout/i.test(e.message)) {
2793
2758
  throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
@@ -2803,56 +2768,74 @@ class Playwright extends Helper {
2803
2768
  async waitForText(text, sec = null, context = null) {
2804
2769
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2805
2770
  const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
2806
- let waiter
2807
2771
 
2808
2772
  const contextObject = await this._getContext()
2809
2773
 
2810
2774
  if (context) {
2811
2775
  const locator = new Locator(context, 'css')
2812
- if (!locator.isXPath()) {
2813
- try {
2814
- await contextObject
2776
+ try {
2777
+ if (!locator.isXPath()) {
2778
+ return contextObject
2815
2779
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2816
2780
  .first()
2817
2781
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2818
- } catch (e) {
2819
- throw new Error(`${errorMessage}\n${e.message}`)
2782
+ .catch(e => {
2783
+ throw new Error(errorMessage)
2784
+ })
2820
2785
  }
2821
- }
2822
2786
 
2823
- if (locator.isXPath()) {
2824
- try {
2825
- await contextObject.waitForFunction(
2826
- ([locator, text, $XPath]) => {
2827
- eval($XPath) // eslint-disable-line no-eval
2828
- const el = $XPath(null, locator)
2829
- if (!el.length) return false
2830
- return el[0].innerText.indexOf(text) > -1
2831
- },
2832
- [locator.value, text, $XPath.toString()],
2833
- { timeout: waitTimeout },
2834
- )
2835
- } catch (e) {
2836
- throw new Error(`${errorMessage}\n${e.message}`)
2787
+ if (locator.isXPath()) {
2788
+ return contextObject
2789
+ .waitForFunction(
2790
+ ([locator, text, $XPath]) => {
2791
+ eval($XPath)
2792
+ const el = $XPath(null, locator)
2793
+ if (!el.length) return false
2794
+ return el[0].innerText.indexOf(text) > -1
2795
+ },
2796
+ [locator.value, text, $XPath.toString()],
2797
+ { timeout: waitTimeout },
2798
+ )
2799
+ .catch(e => {
2800
+ throw new Error(errorMessage)
2801
+ })
2837
2802
  }
2803
+ } catch (e) {
2804
+ throw new Error(`${errorMessage}\n${e.message}`)
2838
2805
  }
2839
- } else {
2840
- // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
2806
+ }
2841
2807
 
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)
2808
+ // Based on original implementation but fixed to check title text and remove problematic promiseRetry
2809
+ // Original used timeoutGap for waitForFunction to give it slightly more time than the locator
2810
+ const timeoutGap = waitTimeout + 1000
2853
2811
 
2854
- if (!waiter) throw new Error(`${errorMessage}`)
2855
- }
2812
+ return Promise.race([
2813
+ // Strategy 1: waitForFunction that checks both body AND title text
2814
+ // Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
2815
+ // Original only checked document.body.innerText, missing title text like "TestEd"
2816
+ this.page.waitForFunction(
2817
+ function (text) {
2818
+ // Check body text (original behavior)
2819
+ if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
2820
+ return true
2821
+ }
2822
+ // Check document title (fixes the TestEd in title issue)
2823
+ if (document.title && document.title.indexOf(text) > -1) {
2824
+ return true
2825
+ }
2826
+ return false
2827
+ },
2828
+ text,
2829
+ { timeout: timeoutGap },
2830
+ ),
2831
+ // Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
2832
+ contextObject
2833
+ .locator(`:has-text(${JSON.stringify(text)})`)
2834
+ .first()
2835
+ .waitFor({ timeout: waitTimeout }),
2836
+ ]).catch(err => {
2837
+ throw new Error(errorMessage)
2838
+ })
2856
2839
  }
2857
2840
 
2858
2841
  /**
@@ -3021,11 +3004,11 @@ class Playwright extends Helper {
3021
3004
  }
3022
3005
  } else {
3023
3006
  const visibleFn = function ([locator, $XPath]) {
3024
- eval($XPath) // eslint-disable-line no-eval
3007
+ eval($XPath)
3025
3008
  return $XPath(null, locator).length === 0
3026
3009
  }
3027
3010
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
3028
- return waiter.catch((err) => {
3011
+ return waiter.catch(err => {
3029
3012
  throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
3030
3013
  })
3031
3014
  }
@@ -3047,9 +3030,9 @@ class Playwright extends Helper {
3047
3030
 
3048
3031
  return promiseRetry(
3049
3032
  async (retry, number) => {
3050
- const _grabCookie = async (name) => {
3033
+ const _grabCookie = async name => {
3051
3034
  const cookies = await this.browserContext.cookies()
3052
- const cookie = cookies.filter((c) => c.name === name)
3035
+ const cookie = cookies.filter(c => c.name === name)
3053
3036
  if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
3054
3037
  }
3055
3038
 
@@ -3128,7 +3111,7 @@ class Playwright extends Helper {
3128
3111
  this.recording = true
3129
3112
  this.recordedAtLeastOnce = true
3130
3113
 
3131
- this.page.on('requestfinished', async (request) => {
3114
+ this.page.on('requestfinished', async request => {
3132
3115
  const information = {
3133
3116
  url: request.url(),
3134
3117
  method: request.method(),
@@ -3167,20 +3150,20 @@ class Playwright extends Helper {
3167
3150
  */
3168
3151
  blockTraffic(urls) {
3169
3152
  if (Array.isArray(urls)) {
3170
- urls.forEach((url) => {
3171
- this.page.route(url, (route) => {
3153
+ urls.forEach(url => {
3154
+ this.page.route(url, route => {
3172
3155
  route
3173
3156
  .abort()
3174
3157
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3175
- .catch((e) => {})
3158
+ .catch(e => {})
3176
3159
  })
3177
3160
  })
3178
3161
  } else {
3179
- this.page.route(urls, (route) => {
3162
+ this.page.route(urls, route => {
3180
3163
  route
3181
3164
  .abort()
3182
3165
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3183
- .catch((e) => {})
3166
+ .catch(e => {})
3184
3167
  })
3185
3168
  }
3186
3169
  }
@@ -3209,8 +3192,8 @@ class Playwright extends Helper {
3209
3192
  urls = [urls]
3210
3193
  }
3211
3194
 
3212
- urls.forEach((url) => {
3213
- this.page.route(url, (route) => {
3195
+ urls.forEach(url => {
3196
+ this.page.route(url, route => {
3214
3197
  if (this.page.isClosed()) {
3215
3198
  // Sometimes it happens that browser has been closed in the meantime.
3216
3199
  // In this case we just don't fulfill to prevent error in test scenario.
@@ -3256,13 +3239,10 @@ class Playwright extends Helper {
3256
3239
  */
3257
3240
  grabTrafficUrl(urlMatch) {
3258
3241
  if (!this.recordedAtLeastOnce) {
3259
- throw new Error(
3260
- 'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
3261
- )
3242
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
3262
3243
  }
3263
3244
 
3264
3245
  for (const i in this.requests) {
3265
- // eslint-disable-next-line no-prototype-builtins
3266
3246
  if (this.requests.hasOwnProperty(i)) {
3267
3247
  const request = this.requests[i]
3268
3248
 
@@ -3312,15 +3292,15 @@ class Playwright extends Helper {
3312
3292
  await this.cdpSession.send('Network.enable')
3313
3293
  await this.cdpSession.send('Page.enable')
3314
3294
 
3315
- this.cdpSession.on('Network.webSocketFrameReceived', (payload) => {
3295
+ this.cdpSession.on('Network.webSocketFrameReceived', payload => {
3316
3296
  this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
3317
3297
  })
3318
3298
 
3319
- this.cdpSession.on('Network.webSocketFrameSent', (payload) => {
3299
+ this.cdpSession.on('Network.webSocketFrameSent', payload => {
3320
3300
  this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
3321
3301
  })
3322
3302
 
3323
- this.cdpSession.on('Network.webSocketFrameError', (payload) => {
3303
+ this.cdpSession.on('Network.webSocketFrameError', payload => {
3324
3304
  this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
3325
3305
  })
3326
3306
  }
@@ -3344,9 +3324,7 @@ class Playwright extends Helper {
3344
3324
  grabWebSocketMessages() {
3345
3325
  if (!this.recordingWebSocketMessages) {
3346
3326
  if (!this.recordedWebSocketMessagesAtLeastOnce) {
3347
- throw new Error(
3348
- 'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
3349
- )
3327
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
3350
3328
  }
3351
3329
  }
3352
3330
  return this.webSocketMessages
@@ -3493,17 +3471,13 @@ async function proceedClick(locator, context = null, options = {}) {
3493
3471
  }
3494
3472
  const els = await findClickable.call(this, matcher, locator)
3495
3473
  if (context) {
3496
- assertElementExists(
3497
- els,
3498
- locator,
3499
- 'Clickable element',
3500
- `was not found inside element ${new Locator(context).toString()}`,
3501
- )
3474
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
3502
3475
  } else {
3503
3476
  assertElementExists(els, locator, 'Clickable element')
3504
3477
  }
3505
3478
 
3506
3479
  await highlightActiveElement.call(this, els[0])
3480
+ if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
3507
3481
 
3508
3482
  /*
3509
3483
  using the force true options itself but instead dispatching a click
@@ -3565,16 +3539,18 @@ async function proceedSee(assertType, text, context, strict = false) {
3565
3539
  description = `element ${locator.toString()}`
3566
3540
  const els = await this._locate(locator)
3567
3541
  assertElementExists(els, locator.toString())
3568
- allText = await Promise.all(els.map((el) => el.innerText()))
3542
+ allText = await Promise.all(els.map(el => el.innerText()))
3543
+ }
3544
+
3545
+ if (store?.currentStep?.opts?.ignoreCase === true) {
3546
+ text = text.toLowerCase()
3547
+ allText = allText.map(elText => elText.toLowerCase())
3569
3548
  }
3570
3549
 
3571
3550
  if (strict) {
3572
- return allText.map((elText) => equals(description)[assertType](text, elText))
3551
+ return allText.map(elText => equals(description)[assertType](text, elText))
3573
3552
  }
3574
- return stringIncludes(description)[assertType](
3575
- normalizeSpacesInString(text),
3576
- normalizeSpacesInString(allText.join(' | ')),
3577
- )
3553
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
3578
3554
  }
3579
3555
 
3580
3556
  async function findCheckable(locator, context) {
@@ -3604,7 +3580,7 @@ async function findCheckable(locator, context) {
3604
3580
  async function proceedIsChecked(assertType, option) {
3605
3581
  let els = await findCheckable.call(this, option)
3606
3582
  assertElementExists(els, option, 'Checkable')
3607
- els = await Promise.all(els.map((el) => el.isChecked()))
3583
+ els = await Promise.all(els.map(el => el.isChecked()))
3608
3584
  const selected = els.reduce((prev, cur) => prev || cur)
3609
3585
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3610
3586
  }
@@ -3636,10 +3612,10 @@ async function proceedSeeInField(assertType, field, value) {
3636
3612
  const els = await findFields.call(this, field)
3637
3613
  assertElementExists(els, field, 'Field')
3638
3614
  const el = els[0]
3639
- const tag = await el.evaluate((e) => e.tagName)
3615
+ const tag = await el.evaluate(e => e.tagName)
3640
3616
  const fieldType = await el.getAttribute('type')
3641
3617
 
3642
- const proceedMultiple = async (elements) => {
3618
+ const proceedMultiple = async elements => {
3643
3619
  const fields = Array.isArray(elements) ? elements : [elements]
3644
3620
 
3645
3621
  const elementValues = []
@@ -3653,7 +3629,7 @@ async function proceedSeeInField(assertType, field, value) {
3653
3629
  if (assertType === 'assert') {
3654
3630
  equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
3655
3631
  }
3656
- elementValues.forEach((val) => stringIncludes(`fields by ${field}`)[assertType](value, val))
3632
+ elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
3657
3633
  }
3658
3634
  }
3659
3635
 
@@ -3740,6 +3716,8 @@ function isFrameLocator(locator) {
3740
3716
  }
3741
3717
 
3742
3718
  function assertElementExists(res, locator, prefix, suffix) {
3719
+ // if element text is an empty string, just exit this check
3720
+ if (typeof res === 'string' && res === '') return
3743
3721
  if (!res || res.length === 0) {
3744
3722
  throw new ElementNotFound(locator, prefix, suffix)
3745
3723
  }
@@ -3776,12 +3754,9 @@ async function targetCreatedHandler(page) {
3776
3754
  this.contextLocator = null
3777
3755
  })
3778
3756
  })
3779
- page.on('console', (msg) => {
3757
+ page.on('console', msg => {
3780
3758
  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
- )
3759
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
3785
3760
  }
3786
3761
  consoleLogStore.add(msg)
3787
3762
  })
@@ -3889,7 +3864,7 @@ async function refreshContextSession() {
3889
3864
  const contexts = await this.browser.contexts()
3890
3865
  contexts.shift()
3891
3866
 
3892
- await Promise.all(contexts.map((c) => c.close()))
3867
+ await Promise.all(contexts.map(c => c.close()))
3893
3868
  } catch (e) {
3894
3869
  console.log(e)
3895
3870
  }
@@ -3908,10 +3883,10 @@ async function refreshContextSession() {
3908
3883
  const currentUrl = await this.grabCurrentUrl()
3909
3884
 
3910
3885
  if (currentUrl.startsWith('http')) {
3911
- await this.executeScript('localStorage.clear();').catch((err) => {
3886
+ await this.executeScript('localStorage.clear();').catch(err => {
3912
3887
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3913
3888
  })
3914
- await this.executeScript('sessionStorage.clear();').catch((err) => {
3889
+ await this.executeScript('sessionStorage.clear();').catch(err => {
3915
3890
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3916
3891
  })
3917
3892
  }
@@ -3935,17 +3910,37 @@ function saveVideoForPage(page, name) {
3935
3910
  async function saveTraceForContext(context, name) {
3936
3911
  if (!context) return
3937
3912
  if (!context.tracing) return
3938
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3939
- await context.tracing.stop({ path: fileName })
3940
- return fileName
3913
+ try {
3914
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3915
+ await context.tracing.stop({ path: fileName })
3916
+ return fileName
3917
+ } catch (err) {
3918
+ // Handle the case where tracing was not started or context is invalid
3919
+ if (err.message && err.message.includes('Must start tracing before stopping')) {
3920
+ // Tracing was never started on this context, silently skip
3921
+ return null
3922
+ }
3923
+ throw err
3924
+ }
3941
3925
  }
3942
3926
 
3943
3927
  async function highlightActiveElement(element) {
3944
- if (this.options.highlightElement && global.debugMode) {
3945
- await element.evaluate((el) => {
3928
+ if ((this.options.highlightElement || store.onPause) && store.debugMode) {
3929
+ await element.evaluate(el => {
3946
3930
  const prevStyle = el.style.boxShadow
3947
- el.style.boxShadow = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)'
3931
+ el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
3948
3932
  setTimeout(() => (el.style.boxShadow = prevStyle), 2000)
3949
3933
  })
3950
3934
  }
3951
3935
  }
3936
+
3937
+ async function elToString(el, numberOfElements) {
3938
+ const html = await el.evaluate(node => node.outerHTML)
3939
+ return (
3940
+ html
3941
+ .replace(/\n/g, '')
3942
+ .replace(/\s+/g, ' ')
3943
+ .substring(0, 100 / numberOfElements)
3944
+ .trim() + '...'
3945
+ )
3946
+ }