codeceptjs 4.0.0-beta.3 → 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 (155) 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 +141 -86
  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/dryRun.js +30 -35
  17. package/lib/command/generate.js +10 -14
  18. package/lib/command/gherkin/snippets.js +75 -73
  19. package/lib/command/gherkin/steps.js +1 -1
  20. package/lib/command/info.js +42 -8
  21. package/lib/command/init.js +13 -12
  22. package/lib/command/interactive.js +10 -2
  23. package/lib/command/list.js +1 -1
  24. package/lib/command/run-multiple/chunk.js +48 -45
  25. package/lib/command/run-multiple.js +12 -35
  26. package/lib/command/run-workers.js +21 -58
  27. package/lib/command/utils.js +5 -6
  28. package/lib/command/workers/runTests.js +263 -222
  29. package/lib/container.js +386 -238
  30. package/lib/data/context.js +10 -13
  31. package/lib/data/dataScenarioConfig.js +8 -8
  32. package/lib/data/dataTableArgument.js +6 -6
  33. package/lib/data/table.js +5 -11
  34. package/lib/effects.js +223 -0
  35. package/lib/element/WebElement.js +327 -0
  36. package/lib/els.js +158 -0
  37. package/lib/event.js +21 -17
  38. package/lib/heal.js +88 -80
  39. package/lib/helper/AI.js +2 -1
  40. package/lib/helper/ApiDataFactory.js +4 -7
  41. package/lib/helper/Appium.js +50 -57
  42. package/lib/helper/FileSystem.js +3 -3
  43. package/lib/helper/GraphQLDataFactory.js +4 -4
  44. package/lib/helper/JSONResponse.js +75 -37
  45. package/lib/helper/Mochawesome.js +31 -9
  46. package/lib/helper/Nightmare.js +37 -58
  47. package/lib/helper/Playwright.js +267 -272
  48. package/lib/helper/Protractor.js +56 -87
  49. package/lib/helper/Puppeteer.js +247 -264
  50. package/lib/helper/REST.js +29 -17
  51. package/lib/helper/TestCafe.js +22 -47
  52. package/lib/helper/WebDriver.js +157 -368
  53. package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
  54. package/lib/helper/extras/Popup.js +22 -22
  55. package/lib/helper/network/utils.js +1 -1
  56. package/lib/helper/testcafe/testcafe-utils.js +27 -28
  57. package/lib/listener/emptyRun.js +55 -0
  58. package/lib/listener/exit.js +7 -10
  59. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  60. package/lib/listener/globalTimeout.js +165 -0
  61. package/lib/listener/helpers.js +15 -15
  62. package/lib/listener/mocha.js +1 -1
  63. package/lib/listener/result.js +12 -0
  64. package/lib/listener/retryEnhancer.js +85 -0
  65. package/lib/listener/steps.js +32 -18
  66. package/lib/listener/store.js +20 -0
  67. package/lib/locator.js +1 -1
  68. package/lib/mocha/asyncWrapper.js +231 -0
  69. package/lib/{interfaces → mocha}/bdd.js +3 -3
  70. package/lib/mocha/cli.js +308 -0
  71. package/lib/mocha/factory.js +104 -0
  72. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  73. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  74. package/lib/mocha/hooks.js +112 -0
  75. package/lib/mocha/index.js +12 -0
  76. package/lib/mocha/inject.js +29 -0
  77. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  78. package/lib/mocha/suite.js +82 -0
  79. package/lib/mocha/test.js +181 -0
  80. package/lib/mocha/types.d.ts +42 -0
  81. package/lib/mocha/ui.js +232 -0
  82. package/lib/output.js +93 -65
  83. package/lib/pause.js +160 -138
  84. package/lib/plugin/analyze.js +396 -0
  85. package/lib/plugin/auth.js +435 -0
  86. package/lib/plugin/autoDelay.js +8 -8
  87. package/lib/plugin/autoLogin.js +3 -338
  88. package/lib/plugin/commentStep.js +6 -1
  89. package/lib/plugin/coverage.js +10 -22
  90. package/lib/plugin/customLocator.js +3 -3
  91. package/lib/plugin/customReporter.js +52 -0
  92. package/lib/plugin/eachElement.js +1 -1
  93. package/lib/plugin/fakerTransform.js +1 -1
  94. package/lib/plugin/heal.js +36 -9
  95. package/lib/plugin/htmlReporter.js +1947 -0
  96. package/lib/plugin/pageInfo.js +140 -0
  97. package/lib/plugin/retryFailedStep.js +17 -18
  98. package/lib/plugin/retryTo.js +2 -113
  99. package/lib/plugin/screenshotOnFail.js +17 -58
  100. package/lib/plugin/selenoid.js +15 -35
  101. package/lib/plugin/standardActingHelpers.js +4 -1
  102. package/lib/plugin/stepByStepReport.js +56 -17
  103. package/lib/plugin/stepTimeout.js +5 -12
  104. package/lib/plugin/subtitles.js +4 -4
  105. package/lib/plugin/tryTo.js +3 -102
  106. package/lib/plugin/wdio.js +8 -10
  107. package/lib/recorder.js +155 -124
  108. package/lib/rerun.js +43 -42
  109. package/lib/result.js +161 -0
  110. package/lib/secret.js +1 -2
  111. package/lib/step/base.js +239 -0
  112. package/lib/step/comment.js +10 -0
  113. package/lib/step/config.js +50 -0
  114. package/lib/step/func.js +46 -0
  115. package/lib/step/helper.js +50 -0
  116. package/lib/step/meta.js +99 -0
  117. package/lib/step/record.js +74 -0
  118. package/lib/step/retry.js +11 -0
  119. package/lib/step/section.js +55 -0
  120. package/lib/step.js +21 -332
  121. package/lib/steps.js +50 -0
  122. package/lib/store.js +37 -5
  123. package/lib/template/heal.js +2 -11
  124. package/lib/test-server.js +323 -0
  125. package/lib/timeout.js +66 -0
  126. package/lib/utils.js +351 -218
  127. package/lib/within.js +75 -55
  128. package/lib/workerStorage.js +2 -1
  129. package/lib/workers.js +386 -277
  130. package/package.json +81 -75
  131. package/translations/de-DE.js +5 -3
  132. package/translations/fr-FR.js +5 -4
  133. package/translations/index.js +1 -0
  134. package/translations/it-IT.js +4 -3
  135. package/translations/ja-JP.js +4 -3
  136. package/translations/nl-NL.js +76 -0
  137. package/translations/pl-PL.js +4 -3
  138. package/translations/pt-BR.js +4 -3
  139. package/translations/ru-RU.js +4 -3
  140. package/translations/utils.js +9 -0
  141. package/translations/zh-CN.js +4 -3
  142. package/translations/zh-TW.js +4 -3
  143. package/typings/index.d.ts +197 -187
  144. package/typings/promiseBasedTypes.d.ts +53 -903
  145. package/typings/types.d.ts +372 -1042
  146. package/lib/cli.js +0 -257
  147. package/lib/helper/ExpectHelper.js +0 -391
  148. package/lib/helper/MockServer.js +0 -221
  149. package/lib/helper/SoftExpectHelper.js +0 -381
  150. package/lib/listener/artifacts.js +0 -19
  151. package/lib/listener/timeout.js +0 -109
  152. package/lib/mochaFactory.js +0 -113
  153. package/lib/plugin/debugErrors.js +0 -67
  154. package/lib/scenario.js +0 -224
  155. 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
 
@@ -1241,14 +1222,13 @@ class Playwright extends Helper {
1241
1222
  * {{> grabPageScrollPosition }}
1242
1223
  */
1243
1224
  async grabPageScrollPosition() {
1244
- /* eslint-disable comma-dangle */
1245
1225
  function getScrollPosition() {
1246
1226
  return {
1247
1227
  x: window.pageXOffset,
1248
1228
  y: window.pageYOffset,
1249
1229
  }
1250
1230
  }
1251
- /* eslint-enable comma-dangle */
1231
+
1252
1232
  return this.executeScript(getScrollPosition)
1253
1233
  }
1254
1234
 
@@ -1284,11 +1264,26 @@ class Playwright extends Helper {
1284
1264
  * ```
1285
1265
  */
1286
1266
  async _locate(locator) {
1287
- const context = (await this.context) || (await this._getContext())
1267
+ const context = await this._getContext()
1288
1268
 
1289
1269
  if (this.frame) return findElements(this.frame, locator)
1290
1270
 
1291
- 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
1292
1287
  }
1293
1288
 
1294
1289
  /**
@@ -1300,7 +1295,7 @@ class Playwright extends Helper {
1300
1295
  * ```
1301
1296
  */
1302
1297
  async _locateElement(locator) {
1303
- const context = (await this.context) || (await this._getContext())
1298
+ const context = await this._getContext()
1304
1299
  return findElement(context, locator)
1305
1300
  }
1306
1301
 
@@ -1347,7 +1342,8 @@ class Playwright extends Helper {
1347
1342
  *
1348
1343
  */
1349
1344
  async grabWebElements(locator) {
1350
- return this._locate(locator)
1345
+ const elements = await this._locate(locator)
1346
+ return elements.map(element => new WebElement(element, this))
1351
1347
  }
1352
1348
 
1353
1349
  /**
@@ -1355,7 +1351,8 @@ class Playwright extends Helper {
1355
1351
  *
1356
1352
  */
1357
1353
  async grabWebElement(locator) {
1358
- return this._locateElement(locator)
1354
+ const element = await this._locateElement(locator)
1355
+ return new WebElement(element, this)
1359
1356
  }
1360
1357
 
1361
1358
  /**
@@ -1438,10 +1435,10 @@ class Playwright extends Helper {
1438
1435
  */
1439
1436
  async closeOtherTabs() {
1440
1437
  const pages = await this.browserContext.pages()
1441
- const otherPages = pages.filter((page) => page !== this.page)
1438
+ const otherPages = pages.filter(page => page !== this.page)
1442
1439
  if (otherPages.length) {
1443
1440
  this.debug(`Closing ${otherPages.length} tabs`)
1444
- return Promise.all(otherPages.map((p) => p.close()))
1441
+ return Promise.all(otherPages.map(p => p.close()))
1445
1442
  }
1446
1443
  return Promise.resolve()
1447
1444
  }
@@ -1484,9 +1481,9 @@ class Playwright extends Helper {
1484
1481
  */
1485
1482
  async seeElement(locator) {
1486
1483
  let els = await this._locate(locator)
1487
- els = await Promise.all(els.map((el) => el.isVisible()))
1484
+ els = await Promise.all(els.map(el => el.isVisible()))
1488
1485
  try {
1489
- return empty('visible elements').negate(els.filter((v) => v).fill('ELEMENT'))
1486
+ return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
1490
1487
  } catch (e) {
1491
1488
  dontSeeElementError(locator)
1492
1489
  }
@@ -1498,9 +1495,9 @@ class Playwright extends Helper {
1498
1495
  */
1499
1496
  async dontSeeElement(locator) {
1500
1497
  let els = await this._locate(locator)
1501
- els = await Promise.all(els.map((el) => el.isVisible()))
1498
+ els = await Promise.all(els.map(el => el.isVisible()))
1502
1499
  try {
1503
- return empty('visible elements').assert(els.filter((v) => v).fill('ELEMENT'))
1500
+ return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
1504
1501
  } catch (e) {
1505
1502
  seeElementError(locator)
1506
1503
  }
@@ -1512,7 +1509,7 @@ class Playwright extends Helper {
1512
1509
  async seeElementInDOM(locator) {
1513
1510
  const els = await this._locate(locator)
1514
1511
  try {
1515
- 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'))
1516
1513
  } catch (e) {
1517
1514
  dontSeeElementInDOMError(locator)
1518
1515
  }
@@ -1524,7 +1521,7 @@ class Playwright extends Helper {
1524
1521
  async dontSeeElementInDOM(locator) {
1525
1522
  const els = await this._locate(locator)
1526
1523
  try {
1527
- 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'))
1528
1525
  } catch (e) {
1529
1526
  seeElementInDOMError(locator)
1530
1527
  }
@@ -1548,7 +1545,7 @@ class Playwright extends Helper {
1548
1545
  * @return {Promise<void>}
1549
1546
  */
1550
1547
  async handleDownloads(fileName) {
1551
- this.page.waitForEvent('download').then(async (download) => {
1548
+ this.page.waitForEvent('download').then(async download => {
1552
1549
  const filePath = await download.path()
1553
1550
  fileName = fileName || `downloads/${path.basename(filePath)}`
1554
1551
 
@@ -1742,6 +1739,7 @@ class Playwright extends Helper {
1742
1739
  const el = els[0]
1743
1740
 
1744
1741
  await el.clear()
1742
+ if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
1745
1743
 
1746
1744
  await highlightActiveElement.call(this, el)
1747
1745
 
@@ -1853,8 +1851,8 @@ class Playwright extends Helper {
1853
1851
  */
1854
1852
  async grabNumberOfVisibleElements(locator) {
1855
1853
  let els = await this._locate(locator)
1856
- els = await Promise.all(els.map((el) => el.isVisible()))
1857
- return els.filter((v) => v).length
1854
+ els = await Promise.all(els.map(el => el.isVisible()))
1855
+ return els.filter(v => v).length
1858
1856
  }
1859
1857
 
1860
1858
  /**
@@ -1964,9 +1962,7 @@ class Playwright extends Helper {
1964
1962
  */
1965
1963
  async seeNumberOfElements(locator, num) {
1966
1964
  const elements = await this._locate(locator)
1967
- return equals(
1968
- `expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`,
1969
- ).assert(elements.length, num)
1965
+ return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
1970
1966
  }
1971
1967
 
1972
1968
  /**
@@ -1976,10 +1972,7 @@ class Playwright extends Helper {
1976
1972
  */
1977
1973
  async seeNumberOfVisibleElements(locator, num) {
1978
1974
  const res = await this.grabNumberOfVisibleElements(locator)
1979
- return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(
1980
- res,
1981
- num,
1982
- )
1975
+ return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
1983
1976
  }
1984
1977
 
1985
1978
  /**
@@ -1998,7 +1991,7 @@ class Playwright extends Helper {
1998
1991
  */
1999
1992
  async seeCookie(name) {
2000
1993
  const cookies = await this.browserContext.cookies()
2001
- 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))
2002
1995
  }
2003
1996
 
2004
1997
  /**
@@ -2006,7 +1999,7 @@ class Playwright extends Helper {
2006
1999
  */
2007
2000
  async dontSeeCookie(name) {
2008
2001
  const cookies = await this.browserContext.cookies()
2009
- 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))
2010
2003
  }
2011
2004
 
2012
2005
  /**
@@ -2017,17 +2010,18 @@ class Playwright extends Helper {
2017
2010
  async grabCookie(name) {
2018
2011
  const cookies = await this.browserContext.cookies()
2019
2012
  if (!name) return cookies
2020
- const cookie = cookies.filter((c) => c.name === name)
2013
+ const cookie = cookies.filter(c => c.name === name)
2021
2014
  if (cookie[0]) return cookie[0]
2022
2015
  }
2023
2016
 
2024
2017
  /**
2025
2018
  * {{> clearCookie }}
2026
2019
  */
2027
- async clearCookie() {
2028
- // Playwright currently doesn't support to delete a certain cookie
2029
- // https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies
2020
+ async clearCookie(cookieName) {
2030
2021
  if (!this.browserContext) return
2022
+ if (cookieName) {
2023
+ return this.browserContext.clearCookies({ name: cookieName })
2024
+ }
2031
2025
  return this.browserContext.clearCookies()
2032
2026
  }
2033
2027
 
@@ -2099,7 +2093,7 @@ class Playwright extends Helper {
2099
2093
  const els = await this._locate(locator)
2100
2094
  const texts = []
2101
2095
  for (const el of els) {
2102
- texts.push(await await el.innerText())
2096
+ texts.push(await el.innerText())
2103
2097
  }
2104
2098
  this.debug(`Matched ${els.length} elements`)
2105
2099
  return texts
@@ -2121,7 +2115,7 @@ class Playwright extends Helper {
2121
2115
  async grabValueFromAll(locator) {
2122
2116
  const els = await findFields.call(this, locator)
2123
2117
  this.debug(`Matched ${els.length} elements`)
2124
- return Promise.all(els.map((el) => el.inputValue()))
2118
+ return Promise.all(els.map(el => el.inputValue()))
2125
2119
  }
2126
2120
 
2127
2121
  /**
@@ -2140,7 +2134,7 @@ class Playwright extends Helper {
2140
2134
  async grabHTMLFromAll(locator) {
2141
2135
  const els = await this._locate(locator)
2142
2136
  this.debug(`Matched ${els.length} elements`)
2143
- return Promise.all(els.map((el) => el.innerHTML()))
2137
+ return Promise.all(els.map(el => el.innerHTML()))
2144
2138
  }
2145
2139
 
2146
2140
  /**
@@ -2161,11 +2155,7 @@ class Playwright extends Helper {
2161
2155
  async grabCssPropertyFromAll(locator, cssProperty) {
2162
2156
  const els = await this._locate(locator)
2163
2157
  this.debug(`Matched ${els.length} elements`)
2164
- const cssValues = await Promise.all(
2165
- els.map((el) =>
2166
- el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty),
2167
- ),
2168
- )
2158
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2169
2159
 
2170
2160
  return cssValues
2171
2161
  }
@@ -2193,19 +2183,16 @@ class Playwright extends Helper {
2193
2183
  }
2194
2184
  }
2195
2185
 
2196
- const values = Object.keys(cssPropertiesCamelCase).map((key) => cssPropertiesCamelCase[key])
2186
+ const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
2197
2187
  if (!Array.isArray(props)) props = [props]
2198
2188
  let chunked = chunkArray(props, values.length)
2199
- chunked = chunked.filter((val) => {
2189
+ chunked = chunked.filter(val => {
2200
2190
  for (let i = 0; i < val.length; ++i) {
2201
- // eslint-disable-next-line eqeqeq
2202
2191
  if (val[i] != values[i]) return false
2203
2192
  }
2204
2193
  return true
2205
2194
  })
2206
- return equals(
2207
- `all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
2208
- ).assert(chunked.length, elemAmount)
2195
+ return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
2209
2196
  }
2210
2197
 
2211
2198
  /**
@@ -2218,16 +2205,16 @@ class Playwright extends Helper {
2218
2205
 
2219
2206
  const elemAmount = res.length
2220
2207
  const commands = []
2221
- res.forEach((el) => {
2222
- Object.keys(attributes).forEach((prop) => {
2208
+ res.forEach(el => {
2209
+ Object.keys(attributes).forEach(prop => {
2223
2210
  commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
2224
2211
  })
2225
2212
  })
2226
2213
  let attrs = await Promise.all(commands)
2227
- const values = Object.keys(attributes).map((key) => attributes[key])
2214
+ const values = Object.keys(attributes).map(key => attributes[key])
2228
2215
  if (!Array.isArray(attrs)) attrs = [attrs]
2229
2216
  let chunked = chunkArray(attrs, values.length)
2230
- chunked = chunked.filter((val) => {
2217
+ chunked = chunked.filter(val => {
2231
2218
  for (let i = 0; i < val.length; ++i) {
2232
2219
  // the attribute could be a boolean
2233
2220
  if (typeof val[i] === 'boolean') return val[i] === values[i]
@@ -2236,10 +2223,7 @@ class Playwright extends Helper {
2236
2223
  }
2237
2224
  return true
2238
2225
  })
2239
- return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
2240
- chunked.length,
2241
- elemAmount,
2242
- )
2226
+ return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
2243
2227
  }
2244
2228
 
2245
2229
  /**
@@ -2312,7 +2296,7 @@ class Playwright extends Helper {
2312
2296
  const fullPageOption = fullPage || this.options.fullPageScreenshots
2313
2297
  let outputFile = screenshotOutputFolder(fileName)
2314
2298
 
2315
- this.debug(`Screenshot is saving to ${outputFile}`)
2299
+ this.debugSection('Screenshot', relativeDir(outputFile))
2316
2300
 
2317
2301
  await this.page.screenshot({
2318
2302
  path: outputFile,
@@ -2325,7 +2309,7 @@ class Playwright extends Helper {
2325
2309
  const activeSessionPage = this.sessionPages[sessionName]
2326
2310
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2327
2311
 
2328
- this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2312
+ this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2329
2313
 
2330
2314
  if (activeSessionPage) {
2331
2315
  await activeSessionPage.screenshot({
@@ -2359,9 +2343,7 @@ class Playwright extends Helper {
2359
2343
  method = method.toLowerCase()
2360
2344
  const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
2361
2345
  if (!allowedMethods.includes(method)) {
2362
- throw new Error(
2363
- `Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`,
2364
- )
2346
+ throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
2365
2347
  }
2366
2348
 
2367
2349
  if (url.startsWith('/')) {
@@ -2398,21 +2380,19 @@ class Playwright extends Helper {
2398
2380
  if (this.options.recordVideo && this.page && this.page.video()) {
2399
2381
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2400
2382
  for (const sessionName in this.sessionPages) {
2401
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2402
- this.sessionPages[sessionName],
2403
- `${test.title}_${sessionName}.failed`,
2404
- )
2383
+ if (sessionName === '') continue
2384
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
2405
2385
  }
2406
2386
  }
2407
2387
 
2408
2388
  if (this.options.trace) {
2409
2389
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2410
2390
  for (const sessionName in this.sessionPages) {
2411
- if (!this.sessionPages[sessionName].context) continue
2412
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2413
- this.sessionPages[sessionName].context,
2414
- `${test.title}_${sessionName}.failed`,
2415
- )
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`)
2416
2396
  }
2417
2397
  }
2418
2398
 
@@ -2426,16 +2406,14 @@ class Playwright extends Helper {
2426
2406
  if (this.options.keepVideoForPassedTests) {
2427
2407
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2428
2408
  for (const sessionName of Object.keys(this.sessionPages)) {
2429
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(
2430
- this.sessionPages[sessionName],
2431
- `${test.title}_${sessionName}.passed`,
2432
- )
2409
+ if (sessionName === '') continue
2410
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
2433
2411
  }
2434
2412
  } else {
2435
2413
  this.page
2436
2414
  .video()
2437
2415
  .delete()
2438
- .catch((e) => {})
2416
+ .catch(e => {})
2439
2417
  }
2440
2418
  }
2441
2419
 
@@ -2444,11 +2422,11 @@ class Playwright extends Helper {
2444
2422
  if (this.options.trace) {
2445
2423
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2446
2424
  for (const sessionName in this.sessionPages) {
2447
- if (!this.sessionPages[sessionName].context) continue
2448
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
2449
- this.sessionPages[sessionName].context,
2450
- `${test.title}_${sessionName}.passed`,
2451
- )
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`)
2452
2430
  }
2453
2431
  }
2454
2432
  } else {
@@ -2465,7 +2443,7 @@ class Playwright extends Helper {
2465
2443
  * {{> wait }}
2466
2444
  */
2467
2445
  async wait(sec) {
2468
- return new Promise((done) => {
2446
+ return new Promise(done => {
2469
2447
  setTimeout(done, sec * 1000)
2470
2448
  })
2471
2449
  }
@@ -2481,20 +2459,18 @@ class Playwright extends Helper {
2481
2459
  const context = await this._getContext()
2482
2460
  if (!locator.isXPath()) {
2483
2461
  const valueFn = function ([locator]) {
2484
- 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
2485
2463
  }
2486
2464
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2487
2465
  } else {
2488
2466
  const enabledFn = function ([locator, $XPath]) {
2489
- eval($XPath) // eslint-disable-line no-eval
2490
- return $XPath(null, locator).filter((el) => !el.disabled).length > 0
2467
+ eval($XPath)
2468
+ return $XPath(null, locator).filter(el => !el.disabled).length > 0
2491
2469
  }
2492
2470
  waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2493
2471
  }
2494
- return waiter.catch((err) => {
2495
- throw new Error(
2496
- `element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2497
- )
2472
+ return waiter.catch(err => {
2473
+ throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2498
2474
  })
2499
2475
  }
2500
2476
 
@@ -2509,20 +2485,18 @@ class Playwright extends Helper {
2509
2485
  const context = await this._getContext()
2510
2486
  if (!locator.isXPath()) {
2511
2487
  const valueFn = function ([locator]) {
2512
- 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
2513
2489
  }
2514
2490
  waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
2515
2491
  } else {
2516
2492
  const disabledFn = function ([locator, $XPath]) {
2517
- eval($XPath) // eslint-disable-line no-eval
2518
- return $XPath(null, locator).filter((el) => el.disabled).length > 0
2493
+ eval($XPath)
2494
+ return $XPath(null, locator).filter(el => el.disabled).length > 0
2519
2495
  }
2520
2496
  waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
2521
2497
  }
2522
- return waiter.catch((err) => {
2523
- throw new Error(
2524
- `element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
2525
- )
2498
+ return waiter.catch(err => {
2499
+ throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2526
2500
  })
2527
2501
  }
2528
2502
 
@@ -2537,26 +2511,21 @@ class Playwright extends Helper {
2537
2511
  const context = await this._getContext()
2538
2512
  if (!locator.isXPath()) {
2539
2513
  const valueFn = function ([locator, value]) {
2540
- return (
2541
- Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
2542
- 0
2543
- )
2514
+ return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2544
2515
  }
2545
2516
  waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
2546
2517
  } else {
2547
2518
  const valueFn = function ([locator, $XPath, value]) {
2548
- eval($XPath) // eslint-disable-line no-eval
2549
- 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
2550
2521
  }
2551
2522
  waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
2552
2523
  timeout: waitTimeout,
2553
2524
  })
2554
2525
  }
2555
- return waiter.catch((err) => {
2526
+ return waiter.catch(err => {
2556
2527
  const loc = locator.toString()
2557
- throw new Error(
2558
- `element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`,
2559
- )
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}`)
2560
2529
  })
2561
2530
  }
2562
2531
 
@@ -2576,22 +2545,20 @@ class Playwright extends Helper {
2576
2545
  if (!els || els.length === 0) {
2577
2546
  return false
2578
2547
  }
2579
- 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
2580
2549
  }
2581
2550
  waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
2582
2551
  } else {
2583
2552
  const visibleFn = function ([locator, $XPath, num]) {
2584
- eval($XPath) // eslint-disable-line no-eval
2585
- 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
2586
2555
  }
2587
2556
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
2588
2557
  timeout: waitTimeout,
2589
2558
  })
2590
2559
  }
2591
- return waiter.catch((err) => {
2592
- throw new Error(
2593
- `The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`,
2594
- )
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}`)
2595
2562
  })
2596
2563
  }
2597
2564
 
@@ -2599,9 +2566,7 @@ class Playwright extends Helper {
2599
2566
  * {{> waitForClickable }}
2600
2567
  */
2601
2568
  async waitForClickable(locator, waitTimeout) {
2602
- console.log(
2603
- 'I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable',
2604
- )
2569
+ console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
2605
2570
  console.log('Remove usage of this function')
2606
2571
  }
2607
2572
 
@@ -2617,9 +2582,7 @@ class Playwright extends Helper {
2617
2582
  try {
2618
2583
  await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2619
2584
  } catch (e) {
2620
- throw new Error(
2621
- `element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
2622
- )
2585
+ throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2623
2586
  }
2624
2587
  }
2625
2588
 
@@ -2711,10 +2674,8 @@ class Playwright extends Helper {
2711
2674
  .locator(buildLocatorString(locator))
2712
2675
  .first()
2713
2676
  .waitFor({ timeout: waitTimeout, state: 'hidden' })
2714
- .catch((err) => {
2715
- throw new Error(
2716
- `element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`,
2717
- )
2677
+ .catch(err => {
2678
+ throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
2718
2679
  })
2719
2680
  }
2720
2681
 
@@ -2737,9 +2698,12 @@ class Playwright extends Helper {
2737
2698
  }
2738
2699
 
2739
2700
  async _getContext() {
2740
- if (this.context && this.context.constructor.name === 'FrameLocator') {
2701
+ if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
2741
2702
  return this.context
2742
2703
  }
2704
+ if (this.frame) {
2705
+ return this.frame
2706
+ }
2743
2707
  return this.page
2744
2708
  }
2745
2709
 
@@ -2751,14 +2715,14 @@ class Playwright extends Helper {
2751
2715
 
2752
2716
  return this.page
2753
2717
  .waitForFunction(
2754
- (urlPart) => {
2718
+ urlPart => {
2755
2719
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2756
2720
  return currUrl.indexOf(urlPart) > -1
2757
2721
  },
2758
2722
  urlPart,
2759
2723
  { timeout: waitTimeout },
2760
2724
  )
2761
- .catch(async (e) => {
2725
+ .catch(async e => {
2762
2726
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2763
2727
  if (/Timeout/i.test(e.message)) {
2764
2728
  throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
@@ -2781,14 +2745,14 @@ class Playwright extends Helper {
2781
2745
 
2782
2746
  return this.page
2783
2747
  .waitForFunction(
2784
- (urlPart) => {
2748
+ urlPart => {
2785
2749
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2786
2750
  return currUrl.indexOf(urlPart) > -1
2787
2751
  },
2788
2752
  urlPart,
2789
2753
  { timeout: waitTimeout },
2790
2754
  )
2791
- .catch(async (e) => {
2755
+ .catch(async e => {
2792
2756
  const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2793
2757
  if (/Timeout/i.test(e.message)) {
2794
2758
  throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
@@ -2804,53 +2768,74 @@ class Playwright extends Helper {
2804
2768
  async waitForText(text, sec = null, context = null) {
2805
2769
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2806
2770
  const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
2807
- let waiter
2808
2771
 
2809
2772
  const contextObject = await this._getContext()
2810
2773
 
2811
2774
  if (context) {
2812
2775
  const locator = new Locator(context, 'css')
2813
- if (!locator.isXPath()) {
2814
- try {
2815
- await contextObject
2776
+ try {
2777
+ if (!locator.isXPath()) {
2778
+ return contextObject
2816
2779
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2817
2780
  .first()
2818
2781
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2819
- } catch (e) {
2820
- throw new Error(`${errorMessage}\n${e.message}`)
2782
+ .catch(e => {
2783
+ throw new Error(errorMessage)
2784
+ })
2821
2785
  }
2822
- }
2823
2786
 
2824
- if (locator.isXPath()) {
2825
- try {
2826
- await contextObject.waitForFunction(
2827
- ([locator, text, $XPath]) => {
2828
- eval($XPath) // eslint-disable-line no-eval
2829
- const el = $XPath(null, locator)
2830
- if (!el.length) return false
2831
- return el[0].innerText.indexOf(text) > -1
2832
- },
2833
- [locator.value, text, $XPath.toString()],
2834
- { timeout: waitTimeout },
2835
- )
2836
- } catch (e) {
2837
- 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
+ })
2838
2802
  }
2803
+ } catch (e) {
2804
+ throw new Error(`${errorMessage}\n${e.message}`)
2839
2805
  }
2840
- } else {
2841
- // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
2842
- // eslint-disable-next-line no-lonely-if
2843
- const _contextObject = this.frame ? this.frame : contextObject
2844
- let count = 0
2845
- do {
2846
- waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible()
2847
- if (waiter) break
2848
- await this.wait(1)
2849
- count += 1000
2850
- } while (count <= waitTimeout)
2851
-
2852
- if (!waiter) throw new Error(`${errorMessage}`)
2853
2806
  }
2807
+
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
2811
+
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
+ })
2854
2839
  }
2855
2840
 
2856
2841
  /**
@@ -3019,11 +3004,11 @@ class Playwright extends Helper {
3019
3004
  }
3020
3005
  } else {
3021
3006
  const visibleFn = function ([locator, $XPath]) {
3022
- eval($XPath) // eslint-disable-line no-eval
3007
+ eval($XPath)
3023
3008
  return $XPath(null, locator).length === 0
3024
3009
  }
3025
3010
  waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
3026
- return waiter.catch((err) => {
3011
+ return waiter.catch(err => {
3027
3012
  throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
3028
3013
  })
3029
3014
  }
@@ -3045,9 +3030,9 @@ class Playwright extends Helper {
3045
3030
 
3046
3031
  return promiseRetry(
3047
3032
  async (retry, number) => {
3048
- const _grabCookie = async (name) => {
3033
+ const _grabCookie = async name => {
3049
3034
  const cookies = await this.browserContext.cookies()
3050
- const cookie = cookies.filter((c) => c.name === name)
3035
+ const cookie = cookies.filter(c => c.name === name)
3051
3036
  if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
3052
3037
  }
3053
3038
 
@@ -3126,7 +3111,7 @@ class Playwright extends Helper {
3126
3111
  this.recording = true
3127
3112
  this.recordedAtLeastOnce = true
3128
3113
 
3129
- this.page.on('requestfinished', async (request) => {
3114
+ this.page.on('requestfinished', async request => {
3130
3115
  const information = {
3131
3116
  url: request.url(),
3132
3117
  method: request.method(),
@@ -3165,20 +3150,20 @@ class Playwright extends Helper {
3165
3150
  */
3166
3151
  blockTraffic(urls) {
3167
3152
  if (Array.isArray(urls)) {
3168
- urls.forEach((url) => {
3169
- this.page.route(url, (route) => {
3153
+ urls.forEach(url => {
3154
+ this.page.route(url, route => {
3170
3155
  route
3171
3156
  .abort()
3172
3157
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3173
- .catch((e) => {})
3158
+ .catch(e => {})
3174
3159
  })
3175
3160
  })
3176
3161
  } else {
3177
- this.page.route(urls, (route) => {
3162
+ this.page.route(urls, route => {
3178
3163
  route
3179
3164
  .abort()
3180
3165
  // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3181
- .catch((e) => {})
3166
+ .catch(e => {})
3182
3167
  })
3183
3168
  }
3184
3169
  }
@@ -3207,8 +3192,8 @@ class Playwright extends Helper {
3207
3192
  urls = [urls]
3208
3193
  }
3209
3194
 
3210
- urls.forEach((url) => {
3211
- this.page.route(url, (route) => {
3195
+ urls.forEach(url => {
3196
+ this.page.route(url, route => {
3212
3197
  if (this.page.isClosed()) {
3213
3198
  // Sometimes it happens that browser has been closed in the meantime.
3214
3199
  // In this case we just don't fulfill to prevent error in test scenario.
@@ -3254,13 +3239,10 @@ class Playwright extends Helper {
3254
3239
  */
3255
3240
  grabTrafficUrl(urlMatch) {
3256
3241
  if (!this.recordedAtLeastOnce) {
3257
- throw new Error(
3258
- 'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
3259
- )
3242
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
3260
3243
  }
3261
3244
 
3262
3245
  for (const i in this.requests) {
3263
- // eslint-disable-next-line no-prototype-builtins
3264
3246
  if (this.requests.hasOwnProperty(i)) {
3265
3247
  const request = this.requests[i]
3266
3248
 
@@ -3310,15 +3292,15 @@ class Playwright extends Helper {
3310
3292
  await this.cdpSession.send('Network.enable')
3311
3293
  await this.cdpSession.send('Page.enable')
3312
3294
 
3313
- this.cdpSession.on('Network.webSocketFrameReceived', (payload) => {
3295
+ this.cdpSession.on('Network.webSocketFrameReceived', payload => {
3314
3296
  this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
3315
3297
  })
3316
3298
 
3317
- this.cdpSession.on('Network.webSocketFrameSent', (payload) => {
3299
+ this.cdpSession.on('Network.webSocketFrameSent', payload => {
3318
3300
  this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
3319
3301
  })
3320
3302
 
3321
- this.cdpSession.on('Network.webSocketFrameError', (payload) => {
3303
+ this.cdpSession.on('Network.webSocketFrameError', payload => {
3322
3304
  this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
3323
3305
  })
3324
3306
  }
@@ -3342,9 +3324,7 @@ class Playwright extends Helper {
3342
3324
  grabWebSocketMessages() {
3343
3325
  if (!this.recordingWebSocketMessages) {
3344
3326
  if (!this.recordedWebSocketMessagesAtLeastOnce) {
3345
- throw new Error(
3346
- 'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
3347
- )
3327
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
3348
3328
  }
3349
3329
  }
3350
3330
  return this.webSocketMessages
@@ -3491,17 +3471,13 @@ async function proceedClick(locator, context = null, options = {}) {
3491
3471
  }
3492
3472
  const els = await findClickable.call(this, matcher, locator)
3493
3473
  if (context) {
3494
- assertElementExists(
3495
- els,
3496
- locator,
3497
- 'Clickable element',
3498
- `was not found inside element ${new Locator(context).toString()}`,
3499
- )
3474
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
3500
3475
  } else {
3501
3476
  assertElementExists(els, locator, 'Clickable element')
3502
3477
  }
3503
3478
 
3504
3479
  await highlightActiveElement.call(this, els[0])
3480
+ if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
3505
3481
 
3506
3482
  /*
3507
3483
  using the force true options itself but instead dispatching a click
@@ -3563,16 +3539,18 @@ async function proceedSee(assertType, text, context, strict = false) {
3563
3539
  description = `element ${locator.toString()}`
3564
3540
  const els = await this._locate(locator)
3565
3541
  assertElementExists(els, locator.toString())
3566
- 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())
3567
3548
  }
3568
3549
 
3569
3550
  if (strict) {
3570
- return allText.map((elText) => equals(description)[assertType](text, elText))
3551
+ return allText.map(elText => equals(description)[assertType](text, elText))
3571
3552
  }
3572
- return stringIncludes(description)[assertType](
3573
- normalizeSpacesInString(text),
3574
- normalizeSpacesInString(allText.join(' | ')),
3575
- )
3553
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
3576
3554
  }
3577
3555
 
3578
3556
  async function findCheckable(locator, context) {
@@ -3602,7 +3580,7 @@ async function findCheckable(locator, context) {
3602
3580
  async function proceedIsChecked(assertType, option) {
3603
3581
  let els = await findCheckable.call(this, option)
3604
3582
  assertElementExists(els, option, 'Checkable')
3605
- els = await Promise.all(els.map((el) => el.isChecked()))
3583
+ els = await Promise.all(els.map(el => el.isChecked()))
3606
3584
  const selected = els.reduce((prev, cur) => prev || cur)
3607
3585
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3608
3586
  }
@@ -3634,10 +3612,10 @@ async function proceedSeeInField(assertType, field, value) {
3634
3612
  const els = await findFields.call(this, field)
3635
3613
  assertElementExists(els, field, 'Field')
3636
3614
  const el = els[0]
3637
- const tag = await el.evaluate((e) => e.tagName)
3615
+ const tag = await el.evaluate(e => e.tagName)
3638
3616
  const fieldType = await el.getAttribute('type')
3639
3617
 
3640
- const proceedMultiple = async (elements) => {
3618
+ const proceedMultiple = async elements => {
3641
3619
  const fields = Array.isArray(elements) ? elements : [elements]
3642
3620
 
3643
3621
  const elementValues = []
@@ -3651,7 +3629,7 @@ async function proceedSeeInField(assertType, field, value) {
3651
3629
  if (assertType === 'assert') {
3652
3630
  equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
3653
3631
  }
3654
- elementValues.forEach((val) => stringIncludes(`fields by ${field}`)[assertType](value, val))
3632
+ elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
3655
3633
  }
3656
3634
  }
3657
3635
 
@@ -3738,6 +3716,8 @@ function isFrameLocator(locator) {
3738
3716
  }
3739
3717
 
3740
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
3741
3721
  if (!res || res.length === 0) {
3742
3722
  throw new ElementNotFound(locator, prefix, suffix)
3743
3723
  }
@@ -3774,12 +3754,9 @@ async function targetCreatedHandler(page) {
3774
3754
  this.contextLocator = null
3775
3755
  })
3776
3756
  })
3777
- page.on('console', (msg) => {
3757
+ page.on('console', msg => {
3778
3758
  if (!consoleLogStore.includes(msg) && this.options.ignoreLog && !this.options.ignoreLog.includes(msg.type())) {
3779
- this.debugSection(
3780
- `Browser:${ucfirst(msg.type())}`,
3781
- ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '),
3782
- )
3759
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
3783
3760
  }
3784
3761
  consoleLogStore.add(msg)
3785
3762
  })
@@ -3813,7 +3790,6 @@ function parseWindowSize(windowSize) {
3813
3790
  // List of key values to key definitions
3814
3791
  // https://github.com/puppeteer/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
3815
3792
  const keyDefinitionMap = {
3816
- /* eslint-disable quote-props */
3817
3793
  0: 'Digit0',
3818
3794
  1: 'Digit1',
3819
3795
  2: 'Digit2',
@@ -3861,7 +3837,6 @@ const keyDefinitionMap = {
3861
3837
  '\\': 'Backslash',
3862
3838
  ']': 'BracketRight',
3863
3839
  "'": 'Quote',
3864
- /* eslint-enable quote-props */
3865
3840
  }
3866
3841
 
3867
3842
  function getNormalizedKey(key) {
@@ -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
+ }