codeceptjs 4.0.0-beta.5 → 4.0.0-beta.6.esm-aria

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 (179) hide show
  1. package/README.md +0 -45
  2. package/bin/codecept.js +46 -57
  3. package/lib/actor.js +15 -11
  4. package/lib/ai.js +6 -5
  5. package/lib/assert/empty.js +9 -8
  6. package/lib/assert/equal.js +15 -17
  7. package/lib/assert/error.js +2 -2
  8. package/lib/assert/include.js +9 -11
  9. package/lib/assert/throws.js +1 -1
  10. package/lib/assert/truth.js +8 -5
  11. package/lib/assert.js +18 -18
  12. package/lib/codecept.js +66 -107
  13. package/lib/colorUtils.js +48 -50
  14. package/lib/command/check.js +32 -27
  15. package/lib/command/configMigrate.js +11 -10
  16. package/lib/command/definitions.js +16 -10
  17. package/lib/command/dryRun.js +16 -16
  18. package/lib/command/generate.js +29 -26
  19. package/lib/command/gherkin/init.js +36 -38
  20. package/lib/command/gherkin/snippets.js +14 -14
  21. package/lib/command/gherkin/steps.js +21 -18
  22. package/lib/command/info.js +8 -8
  23. package/lib/command/init.js +34 -31
  24. package/lib/command/interactive.js +11 -10
  25. package/lib/command/list.js +10 -9
  26. package/lib/command/run-multiple/chunk.js +5 -5
  27. package/lib/command/run-multiple/collection.js +5 -5
  28. package/lib/command/run-multiple/run.js +3 -3
  29. package/lib/command/run-multiple.js +16 -13
  30. package/lib/command/run-rerun.js +6 -7
  31. package/lib/command/run-workers.js +10 -24
  32. package/lib/command/run.js +8 -8
  33. package/lib/command/utils.js +20 -18
  34. package/lib/command/workers/runTests.js +117 -269
  35. package/lib/config.js +111 -49
  36. package/lib/container.js +299 -102
  37. package/lib/data/context.js +6 -5
  38. package/lib/data/dataScenarioConfig.js +1 -1
  39. package/lib/data/dataTableArgument.js +1 -1
  40. package/lib/data/table.js +1 -1
  41. package/lib/effects.js +94 -10
  42. package/lib/els.js +11 -9
  43. package/lib/event.js +11 -10
  44. package/lib/globals.js +141 -0
  45. package/lib/heal.js +12 -12
  46. package/lib/helper/AI.js +1 -1
  47. package/lib/helper/ApiDataFactory.js +16 -13
  48. package/lib/helper/FileSystem.js +32 -12
  49. package/lib/helper/GraphQL.js +1 -1
  50. package/lib/helper/GraphQLDataFactory.js +1 -1
  51. package/lib/helper/JSONResponse.js +19 -30
  52. package/lib/helper/Mochawesome.js +9 -28
  53. package/lib/helper/Playwright.js +668 -265
  54. package/lib/helper/Puppeteer.js +284 -169
  55. package/lib/helper/REST.js +29 -12
  56. package/lib/helper/WebDriver.js +191 -71
  57. package/lib/helper/errors/ConnectionRefused.js +6 -6
  58. package/lib/helper/errors/ElementAssertion.js +11 -16
  59. package/lib/helper/errors/ElementNotFound.js +5 -9
  60. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  61. package/lib/helper/extras/Console.js +11 -11
  62. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  63. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  64. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  65. package/lib/helper/extras/Popup.js +1 -1
  66. package/lib/helper/extras/React.js +29 -30
  67. package/lib/helper/network/actions.js +33 -48
  68. package/lib/helper/network/utils.js +76 -83
  69. package/lib/helper/scripts/blurElement.js +6 -6
  70. package/lib/helper/scripts/focusElement.js +6 -6
  71. package/lib/helper/scripts/highlightElement.js +9 -9
  72. package/lib/helper/scripts/isElementClickable.js +34 -34
  73. package/lib/helper.js +2 -1
  74. package/lib/history.js +23 -20
  75. package/lib/hooks.js +10 -10
  76. package/lib/html.js +90 -100
  77. package/lib/index.js +48 -21
  78. package/lib/listener/config.js +8 -9
  79. package/lib/listener/emptyRun.js +6 -7
  80. package/lib/listener/exit.js +4 -3
  81. package/lib/listener/globalRetry.js +5 -5
  82. package/lib/listener/globalTimeout.js +11 -10
  83. package/lib/listener/helpers.js +33 -14
  84. package/lib/listener/mocha.js +3 -4
  85. package/lib/listener/result.js +4 -5
  86. package/lib/listener/steps.js +7 -18
  87. package/lib/listener/store.js +3 -3
  88. package/lib/locator.js +213 -192
  89. package/lib/mocha/asyncWrapper.js +108 -75
  90. package/lib/mocha/bdd.js +99 -13
  91. package/lib/mocha/cli.js +60 -27
  92. package/lib/mocha/factory.js +75 -19
  93. package/lib/mocha/featureConfig.js +1 -1
  94. package/lib/mocha/gherkin.js +57 -25
  95. package/lib/mocha/hooks.js +12 -3
  96. package/lib/mocha/index.js +13 -4
  97. package/lib/mocha/inject.js +22 -5
  98. package/lib/mocha/scenarioConfig.js +2 -2
  99. package/lib/mocha/suite.js +9 -2
  100. package/lib/mocha/test.js +10 -13
  101. package/lib/mocha/ui.js +28 -31
  102. package/lib/output.js +11 -9
  103. package/lib/parser.js +44 -44
  104. package/lib/pause.js +15 -16
  105. package/lib/plugin/analyze.js +19 -12
  106. package/lib/plugin/auth.js +20 -21
  107. package/lib/plugin/autoDelay.js +12 -8
  108. package/lib/plugin/coverage.js +12 -8
  109. package/lib/plugin/customLocator.js +3 -3
  110. package/lib/plugin/customReporter.js +3 -2
  111. package/lib/plugin/heal.js +14 -9
  112. package/lib/plugin/pageInfo.js +10 -10
  113. package/lib/plugin/pauseOnFail.js +4 -3
  114. package/lib/plugin/retryFailedStep.js +47 -5
  115. package/lib/plugin/screenshotOnFail.js +75 -37
  116. package/lib/plugin/stepByStepReport.js +14 -14
  117. package/lib/plugin/stepTimeout.js +4 -3
  118. package/lib/plugin/subtitles.js +6 -5
  119. package/lib/recorder.js +33 -23
  120. package/lib/rerun.js +69 -26
  121. package/lib/result.js +4 -4
  122. package/lib/secret.js +18 -17
  123. package/lib/session.js +95 -89
  124. package/lib/step/base.js +6 -6
  125. package/lib/step/config.js +1 -1
  126. package/lib/step/func.js +3 -3
  127. package/lib/step/helper.js +3 -3
  128. package/lib/step/meta.js +4 -4
  129. package/lib/step/record.js +11 -11
  130. package/lib/step/retry.js +3 -3
  131. package/lib/step/section.js +3 -3
  132. package/lib/step.js +7 -10
  133. package/lib/steps.js +9 -5
  134. package/lib/store.js +1 -1
  135. package/lib/timeout.js +1 -7
  136. package/lib/transform.js +8 -8
  137. package/lib/translation.js +32 -18
  138. package/lib/utils.js +68 -97
  139. package/lib/workerStorage.js +16 -17
  140. package/lib/workers.js +145 -171
  141. package/package.json +63 -57
  142. package/translations/de-DE.js +2 -2
  143. package/translations/fr-FR.js +2 -2
  144. package/translations/index.js +23 -10
  145. package/translations/it-IT.js +2 -2
  146. package/translations/ja-JP.js +2 -2
  147. package/translations/nl-NL.js +2 -2
  148. package/translations/pl-PL.js +2 -2
  149. package/translations/pt-BR.js +2 -2
  150. package/translations/ru-RU.js +2 -2
  151. package/translations/utils.js +11 -2
  152. package/translations/zh-CN.js +2 -2
  153. package/translations/zh-TW.js +2 -2
  154. package/typings/index.d.ts +7 -18
  155. package/typings/promiseBasedTypes.d.ts +3769 -5450
  156. package/typings/types.d.ts +3953 -5778
  157. package/bin/test-server.js +0 -53
  158. package/lib/element/WebElement.js +0 -327
  159. package/lib/helper/Nightmare.js +0 -1486
  160. package/lib/helper/Protractor.js +0 -1840
  161. package/lib/helper/TestCafe.js +0 -1391
  162. package/lib/helper/clientscripts/nightmare.js +0 -213
  163. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  164. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  165. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  166. package/lib/listener/retryEnhancer.js +0 -85
  167. package/lib/plugin/allure.js +0 -15
  168. package/lib/plugin/autoLogin.js +0 -5
  169. package/lib/plugin/commentStep.js +0 -141
  170. package/lib/plugin/eachElement.js +0 -127
  171. package/lib/plugin/fakerTransform.js +0 -49
  172. package/lib/plugin/htmlReporter.js +0 -1947
  173. package/lib/plugin/retryTo.js +0 -16
  174. package/lib/plugin/selenoid.js +0 -364
  175. package/lib/plugin/standardActingHelpers.js +0 -6
  176. package/lib/plugin/tryTo.js +0 -16
  177. package/lib/plugin/wdio.js +0 -247
  178. package/lib/test-server.js +0 -323
  179. package/lib/within.js +0 -90
@@ -1,19 +1,17 @@
1
- const path = require('path')
2
- const fs = require('fs')
3
-
4
- const Helper = require('@codeceptjs/helper')
5
- const { v4: uuidv4 } = require('uuid')
6
- const assert = require('assert')
7
- const promiseRetry = require('promise-retry')
8
- const Locator = require('../locator')
9
- const recorder = require('../recorder')
10
- const store = require('../store')
11
- const stringIncludes = require('../assert/include').includes
12
- const { urlEquals } = require('../assert/equal')
13
- const { equals } = require('../assert/equal')
14
- const { empty } = require('../assert/empty')
15
- const { truth } = require('../assert/truth')
16
- const {
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import Helper from '@codeceptjs/helper'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import assert from 'assert'
6
+ import promiseRetry from 'promise-retry'
7
+ import Locator from '../locator.js'
8
+ import recorder from '../recorder.js'
9
+ import store from '../store.js'
10
+ import { includes as stringIncludes } from '../assert/include.js'
11
+ import { urlEquals, equals } from '../assert/equal.js'
12
+ import { empty } from '../assert/empty.js'
13
+ import { truth } from '../assert/truth.js'
14
+ import {
17
15
  xpathLocator,
18
16
  ucfirst,
19
17
  fileExists,
@@ -26,14 +24,13 @@ const {
26
24
  requireWithFallback,
27
25
  normalizeSpacesInString,
28
26
  relativeDir,
29
- } = require('../utils')
30
- const { isColorProperty, convertColorToRGBA } = require('../colorUtils')
31
- const ElementNotFound = require('./errors/ElementNotFound')
32
- const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused')
33
- const Popup = require('./extras/Popup')
34
- const Console = require('./extras/Console')
35
- const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator')
36
- const WebElement = require('../element/WebElement')
27
+ } from '../utils.js'
28
+ import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
29
+ import ElementNotFound from './errors/ElementNotFound.js'
30
+ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
31
+ import Popup from './extras/Popup.js'
32
+ import Console from './extras/Console.js'
33
+ import { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole } from './extras/PlaywrightLocator.js'
37
34
 
38
35
  let playwright
39
36
  let perfTiming
@@ -43,10 +40,10 @@ const popupStore = new Popup()
43
40
  const consoleLogStore = new Console()
44
41
  const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
45
42
 
46
- const { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts')
47
- const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine')
48
- const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
49
- const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
43
+ import { setRestartStrategy, restartsSession, restartsContext } from './extras/PlaywrightRestartOpts.js'
44
+ import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'
45
+ import { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
46
+ import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
50
47
 
51
48
  const pathSeparator = path.sep
52
49
 
@@ -62,7 +59,6 @@ const pathSeparator = path.sep
62
59
  * @prop {boolean} [show=true] - show browser window.
63
60
  * @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
64
61
  * * 'context' or **false** - restarts [browser context](https://playwright.dev/docs/api/class-browsercontext) but keeps running browser. Recommended by Playwright team to keep tests isolated.
65
- * * 'browser' or **true** - closes browser and opens it again between tests.
66
62
  * * 'session' or 'keep' - keeps browser context and session, but cleans up cookies and localStorage between tests. The fastest option when running tests in windowed mode. Works with `keepCookies` and `keepBrowserState` options. This behavior was default before CodeceptJS 3.1
67
63
  * @prop {number} [timeout=1000] - - [timeout](https://playwright.dev/docs/api/class-page#page-set-default-timeout) in ms of all Playwright actions .
68
64
  * @prop {boolean} [disableScreenshots=false] - don't save screenshot on failure.
@@ -321,7 +317,7 @@ class Playwright extends Helper {
321
317
  constructor(config) {
322
318
  super(config)
323
319
 
324
- playwright = requireWithFallback('playwright', 'playwright-core')
320
+ // playwright will be loaded dynamically in _init method
325
321
 
326
322
  // set defaults
327
323
  this.isRemoteBrowser = false
@@ -345,6 +341,10 @@ class Playwright extends Helper {
345
341
  this.recordedWebSocketMessagesAtLeastOnce = false
346
342
  this.cdpSession = null
347
343
 
344
+ // Add test failure tracking to prevent false positives
345
+ this.testFailures = []
346
+ this.hasCleanupError = false
347
+
348
348
  // override defaults with config
349
349
  this._setConfig(config)
350
350
  }
@@ -455,13 +455,30 @@ class Playwright extends Helper {
455
455
 
456
456
  static _checkRequirements() {
457
457
  try {
458
- requireWithFallback('playwright', 'playwright-core')
458
+ // In ESM, playwright will be checked via dynamic import in constructor
459
+ // The import will fail at module load time if playwright is missing
460
+ return null
459
461
  } catch (e) {
460
462
  return ['playwright@^1.18']
461
463
  }
462
464
  }
463
465
 
464
466
  async _init() {
467
+ // Load playwright dynamically with fallback
468
+ if (!playwright) {
469
+ try {
470
+ playwright = await import('playwright')
471
+ playwright = playwright.default || playwright
472
+ } catch (e) {
473
+ try {
474
+ playwright = await import('playwright-core')
475
+ playwright = playwright.default || playwright
476
+ } catch (e2) {
477
+ throw new Error('Neither playwright nor playwright-core could be loaded. Please install one of them.')
478
+ }
479
+ }
480
+ }
481
+
465
482
  // register an internal selector engine for reading value property of elements in a selector
466
483
  if (defaultSelectorEnginesInitialized) return
467
484
  defaultSelectorEnginesInitialized = true
@@ -475,7 +492,9 @@ class Playwright extends Helper {
475
492
  }
476
493
 
477
494
  _beforeSuite() {
478
- if ((restartsSession() || restartsContext()) && !this.options.manualStart && !this.isRunning) {
495
+ // Start browser if not manually started and not already running
496
+ // Browser should start in singleton mode (restart: false) or when restart strategy is enabled
497
+ if (!this.options.manualStart && !this.isRunning) {
479
498
  this.debugSection('Session', 'Starting singleton browser session')
480
499
  return this._startBrowser()
481
500
  }
@@ -484,6 +503,18 @@ class Playwright extends Helper {
484
503
  async _before(test) {
485
504
  this.currentRunningTest = test
486
505
 
506
+ // Reset failure tracking for each test to prevent false positives
507
+ this.hasCleanupError = false
508
+ this.testFailures = []
509
+
510
+ // Reset frame context to ensure clean state for each test
511
+ this.context = this.page
512
+ this.frame = null
513
+ this.contextLocator = null
514
+
515
+ // Clear popup state to ensure clean state for each test
516
+ popupStore.clear()
517
+
487
518
  recorder.retry({
488
519
  retries: test?.opts?.conditionalRetries || 3,
489
520
  when: err => {
@@ -495,7 +526,6 @@ class Playwright extends Helper {
495
526
  },
496
527
  })
497
528
 
498
- if (restartsBrowser() && !this.options.manualStart) await this._startBrowser()
499
529
  if (!this.isRunning && !this.options.manualStart) await this._startBrowser()
500
530
 
501
531
  this.isAuthenticated = false
@@ -534,6 +564,14 @@ class Playwright extends Helper {
534
564
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
535
565
  this.contextOptions = contextOptions
536
566
  if (!this.browserContext || !restartsSession()) {
567
+ if (!this.browser) {
568
+ if (this.options.manualStart) {
569
+ this.debugSection('Manual Start', 'Browser not started - skipping context creation')
570
+ return // Skip context creation when manualStart is true
571
+ } else {
572
+ throw new Error('Browser not started. This should not happen.')
573
+ }
574
+ }
537
575
  this.debugSection('New Session', JSON.stringify(this.contextOptions))
538
576
  this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
539
577
  }
@@ -577,8 +615,12 @@ class Playwright extends Helper {
577
615
  if (!this.isRunning) return
578
616
 
579
617
  if (this.isElectron) {
580
- this.browser.close()
581
- this.electronSessions.forEach(session => session.close())
618
+ try {
619
+ this.browser.close()
620
+ this.electronSessions.forEach(session => session.close())
621
+ } catch (e) {
622
+ console.warn('Warning during electron cleanup:', e.message)
623
+ }
582
624
  return
583
625
  }
584
626
 
@@ -586,34 +628,154 @@ class Playwright extends Helper {
586
628
  return refreshContextSession.bind(this)()
587
629
  }
588
630
 
589
- if (restartsBrowser()) {
590
- this.isRunning = false
591
- return this._stopBrowser()
631
+ // close other sessions with timeout protection, but only if restartsContext() is true
632
+ if (restartsContext()) {
633
+ try {
634
+ if ((await this.browser)?._type === 'Browser') {
635
+ const contexts = await Promise.race([this.browser.contexts(), new Promise((_, reject) => setTimeout(() => reject(new Error('Get contexts timeout')), 3000))])
636
+ const currentContext = contexts[0]
637
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
638
+ try {
639
+ this.storageState = await currentContext.storageState()
640
+ } catch (e) {
641
+ console.warn('Warning during storage state save:', e.message)
642
+ }
643
+ }
644
+
645
+ await Promise.race([Promise.all(contexts.map(c => c.close())), new Promise((_, reject) => setTimeout(() => reject(new Error('Close contexts timeout')), 5000))])
646
+ }
647
+ } catch (e) {
648
+ console.warn('Warning during context cleanup in _after:', e.message)
649
+ }
650
+ }
651
+
652
+ return this.browser
653
+ }
654
+
655
+ async _afterSuite() {
656
+ // Only stop browser if restart strategy requires it
657
+ if ((restartsSession() || restartsContext()) && this.isRunning) {
658
+ try {
659
+ await this._stopBrowser()
660
+ } catch (e) {
661
+ console.warn('Warning during suite cleanup:', e.message)
662
+ // Track suite cleanup failures
663
+ this.hasCleanupError = true
664
+ this.testFailures.push(`Suite cleanup failed: ${e.message}`)
665
+ } finally {
666
+ this.isRunning = false
667
+ }
592
668
  }
593
669
 
594
- // close other sessions
670
+ // Force cleanup of any remaining browser processes
595
671
  try {
596
- if ((await this.browser)._type === 'Browser') {
597
- const contexts = await this.browser.contexts()
598
- const currentContext = contexts[0]
599
- if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
600
- this.storageState = await currentContext.storageState()
672
+ if (this.browser && (!this.browser.isConnected || this.browser)) {
673
+ await Promise.race([Promise.resolve(), new Promise(resolve => setTimeout(resolve, 1000))])
674
+ }
675
+ } catch (e) {
676
+ console.warn('Final cleanup warning:', e.message)
677
+ this.hasCleanupError = true
678
+ this.testFailures.push(`Final cleanup failed: ${e.message}`)
679
+ }
680
+
681
+ // Clean up session pages explicitly to prevent hanging references
682
+ try {
683
+ if (this.sessionPages && Object.keys(this.sessionPages).length > 0) {
684
+ for (const sessionName in this.sessionPages) {
685
+ const sessionPage = this.sessionPages[sessionName]
686
+ if (sessionPage && !sessionPage.isClosed()) {
687
+ try {
688
+ // Remove any remaining event listeners from session pages
689
+ sessionPage.removeAllListeners('dialog')
690
+ sessionPage.removeAllListeners('crash')
691
+ sessionPage.removeAllListeners('close')
692
+ sessionPage.removeAllListeners('error')
693
+ await sessionPage.close()
694
+ } catch (e) {
695
+ console.warn(`Warning closing session page ${sessionName}:`, e.message)
696
+ }
697
+ }
601
698
  }
699
+ this.sessionPages = {} // Clear the session pages object
700
+ this.activeSessionName = '' // Reset active session name
701
+ }
702
+ } catch (e) {
703
+ console.warn('Session pages cleanup warning:', e.message)
704
+ this.hasCleanupError = true
705
+ this.testFailures.push(`Session cleanup failed: ${e.message}`)
706
+ }
602
707
 
603
- await Promise.all(contexts.map(c => c.close()))
708
+ // Clear any lingering DOM timeouts by executing cleanup in browser context
709
+ try {
710
+ if (this.page && !this.page.isClosed()) {
711
+ await this.page
712
+ .evaluate(() => {
713
+ // Clear any running highlight timeouts by clearing a range of timeout IDs
714
+ for (let i = 1; i <= 1000; i++) {
715
+ clearTimeout(i)
716
+ }
717
+ })
718
+ .catch(() => {
719
+ // Ignore errors if execution context is destroyed (e.g., due to navigation)
720
+ })
604
721
  }
605
722
  } catch (e) {
606
- console.log(e)
723
+ // Only log if it's not an execution context error
724
+ if (!e.message.includes('Execution context was destroyed')) {
725
+ console.warn('DOM timeout cleanup warning:', e.message)
726
+ this.hasCleanupError = true
727
+ this.testFailures.push(`DOM cleanup failed: ${e.message}`)
728
+ }
607
729
  }
608
730
 
609
- // await this.closeOtherTabs();
610
- return this.browser
731
+ // If we have cleanup errors, throw to fail the test suite
732
+ if (this.hasCleanupError && this.testFailures.length > 0) {
733
+ const errorMessage = `Test suite cleanup failed: ${this.testFailures.join('; ')}`
734
+ console.error(errorMessage)
735
+ throw new Error(errorMessage)
736
+ }
611
737
  }
612
738
 
613
- _afterSuite() {}
614
-
615
739
  async _finishTest() {
616
- if ((restartsSession() || restartsContext()) && this.isRunning) return this._stopBrowser()
740
+ if ((restartsSession() || restartsContext()) && this.isRunning) {
741
+ try {
742
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Test finish timeout')), 10000))])
743
+ } catch (e) {
744
+ console.warn('Warning during test finish cleanup:', e.message)
745
+ // Track cleanup failures to prevent false positives
746
+ this.hasCleanupError = true
747
+ this.testFailures.push(`Test finish cleanup failed: ${e.message}`)
748
+
749
+ this.isRunning = false
750
+ // Set flags to prevent further operations after cleanup failure
751
+ this.page = null
752
+ this.browserContext = null
753
+ this.browser = null
754
+
755
+ // Propagate the error to fail the test properly
756
+ throw new Error(`Test cleanup failed: ${e.message}`)
757
+ }
758
+ }
759
+ }
760
+
761
+ async _cleanup() {
762
+ // Final cleanup when test run completes
763
+ if (this.isRunning) {
764
+ try {
765
+ await this._stopBrowser()
766
+ } catch (e) {
767
+ console.warn('Warning during final cleanup:', e.message)
768
+ }
769
+ } else {
770
+ // Check if we still have a browser object despite isRunning being false
771
+ if (this.browser) {
772
+ try {
773
+ await this._stopBrowser()
774
+ } catch (e) {
775
+ console.warn('Warning during forced cleanup:', e.message)
776
+ }
777
+ }
778
+ }
617
779
  }
618
780
 
619
781
  _session() {
@@ -632,13 +794,20 @@ class Playwright extends Helper {
632
794
  page = await browser.firstWindow()
633
795
  } else {
634
796
  try {
635
- browserContext = await this.browser.newContext(Object.assign(this.contextOptions, config))
636
- page = await browserContext.newPage()
797
+ // Check if browser is still available before creating context
798
+ if (!this.browser) {
799
+ throw new Error('Browser is not available for session context creation')
800
+ }
801
+ browserContext = await Promise.race([this.browser.newContext(Object.assign(this.contextOptions, config)), new Promise((_, reject) => setTimeout(() => reject(new Error('New context timeout')), 10000))])
802
+ page = await Promise.race([browserContext.newPage(), new Promise((_, reject) => setTimeout(() => reject(new Error('New page timeout')), 5000))])
637
803
  } catch (e) {
804
+ console.warn('Warning during context creation:', e.message)
638
805
  if (this.playwrightOptions.userDataDir) {
639
806
  browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
640
807
  this.browser = browserContext
641
808
  page = await browserContext.pages()[0]
809
+ } else {
810
+ throw e
642
811
  }
643
812
  }
644
813
  }
@@ -669,8 +838,28 @@ class Playwright extends Helper {
669
838
  } else {
670
839
  this.activeSessionName = session
671
840
  }
672
- const existingPages = await this.browserContext.pages()
673
- await this._setPage(existingPages[0])
841
+
842
+ // Safety check: ensure browserContext exists before calling pages()
843
+ if (!this.browserContext) {
844
+ this.debug('Cannot restore session vars: browserContext is undefined')
845
+ return
846
+ }
847
+
848
+ try {
849
+ const existingPages = await this.browserContext.pages()
850
+ if (existingPages && existingPages.length > 0) {
851
+ await this._setPage(existingPages[0])
852
+ // Reset context-related variables to ensure clean state after session
853
+ this.context = await this.page
854
+ this.contextLocator = null
855
+ this.frame = null
856
+ } else {
857
+ this.debug('Cannot restore session vars: no pages available')
858
+ }
859
+ } catch (err) {
860
+ this.debug(`Failed to restore session vars: ${err.message}`)
861
+ return
862
+ }
674
863
 
675
864
  return this._waitForAction()
676
865
  },
@@ -756,21 +945,43 @@ class Playwright extends Helper {
756
945
  * @param {object} page page to set
757
946
  */
758
947
  async _setPage(page) {
948
+ // Clean up previous page event listeners
949
+ if (this.page && this.page !== page) {
950
+ try {
951
+ this.page.removeAllListeners('crash')
952
+ this.page.removeAllListeners('dialog')
953
+ } catch (e) {
954
+ console.warn('Warning cleaning previous page listeners:', e.message)
955
+ }
956
+ }
957
+
759
958
  page = await page
760
959
  this._addPopupListener(page)
761
960
  this.page = page
762
961
  if (!page) return
763
- this.browserContext.setDefaultTimeout(0)
764
- page.setDefaultNavigationTimeout(this.options.getPageTimeout)
765
- page.setDefaultTimeout(this.options.timeout)
766
962
 
767
- page.on('crash', async () => {
768
- console.log('ERROR: Page has crashed, closing page!')
769
- await page.close()
770
- })
771
- this.context = await this.page
772
- this.contextLocator = null
773
- await page.bringToFront()
963
+ try {
964
+ this.browserContext.setDefaultTimeout(0)
965
+ page.setDefaultNavigationTimeout(this.options.getPageTimeout)
966
+ page.setDefaultTimeout(this.options.timeout)
967
+
968
+ page.on('crash', async () => {
969
+ console.log('ERROR: Page has crashed, closing page!')
970
+ try {
971
+ await page.close()
972
+ } catch (e) {
973
+ console.warn('Warning during crashed page cleanup:', e.message)
974
+ }
975
+ })
976
+
977
+ this.context = await this.page
978
+ this.contextLocator = null
979
+ await page.bringToFront()
980
+ } catch (e) {
981
+ console.warn('Warning during page setup:', e.message)
982
+ this.context = await this.page
983
+ this.contextLocator = null
984
+ }
774
985
  }
775
986
 
776
987
  /**
@@ -868,6 +1079,9 @@ class Playwright extends Helper {
868
1079
  * @param {object} [contextOptions] See https://playwright.dev/docs/api/class-browser#browser-new-context
869
1080
  */
870
1081
  async _createContextPage(contextOptions) {
1082
+ if (!this.browser) {
1083
+ throw new Error('Browser not started. Call _startBrowser() first or disable manualStart option.')
1084
+ }
871
1085
  this.browserContext = await this.browser.newContext(contextOptions)
872
1086
  const page = await this.browserContext.newPage()
873
1087
  targetCreatedHandler.call(this, page)
@@ -884,8 +1098,58 @@ class Playwright extends Helper {
884
1098
  this.context = null
885
1099
  this.frame = null
886
1100
  popupStore.clear()
887
- if (this.options.recordHar) await this.browserContext.close()
888
- await this.browser.close()
1101
+
1102
+ // Clean up event listeners to prevent hanging
1103
+ try {
1104
+ if (this.browser) {
1105
+ this.browser.removeAllListeners('targetchanged')
1106
+ if (this.browserContext) {
1107
+ // Clean up any page event listeners in the context
1108
+ const pages = this.browserContext.pages()
1109
+ for (const page of pages) {
1110
+ try {
1111
+ page.removeAllListeners('crash')
1112
+ page.removeAllListeners('dialog')
1113
+ } catch (e) {
1114
+ console.warn('Warning cleaning page listeners:', e.message)
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ } catch (e) {
1120
+ console.warn('Warning cleaning event listeners:', e.message)
1121
+ }
1122
+
1123
+ try {
1124
+ if (this.browserContext) {
1125
+ await Promise.race([this.browserContext.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Context close timeout')), 3000))])
1126
+ }
1127
+ } catch (error) {
1128
+ console.warn('Failed to close browser context:', error.message)
1129
+ }
1130
+
1131
+ try {
1132
+ if (this.browser) {
1133
+ await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 3000))])
1134
+ }
1135
+ } catch (error) {
1136
+ console.warn('Failed to close browser:', error.message)
1137
+ }
1138
+
1139
+ // Always try to kill the browser process to ensure cleanup
1140
+ try {
1141
+ if (this.browser && this.browser.process && this.browser.process()) {
1142
+ this.browser.process().kill('SIGKILL')
1143
+ }
1144
+ } catch (e) {
1145
+ // Silently ignore process kill errors
1146
+ }
1147
+
1148
+ // Ensure cleanup is complete
1149
+ this.browser = null
1150
+ this.browserContext = null
1151
+ this.page = null
1152
+ this.isRunning = false
889
1153
  }
890
1154
 
891
1155
  async _evaluateHandeInContext(...args) {
@@ -902,8 +1166,21 @@ class Playwright extends Helper {
902
1166
 
903
1167
  if (frame) {
904
1168
  if (Array.isArray(frame)) {
1169
+ // For nested frames, build the complete frame path
905
1170
  await this.switchTo(null)
906
- return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve())
1171
+
1172
+ // Build nested frame locator from page
1173
+ let frameLocatorObj = this.page
1174
+ for (const frameSelector of frame) {
1175
+ const selector = buildLocatorString(new Locator(frameSelector, 'css'))
1176
+ frameLocatorObj = frameLocatorObj.frameLocator(selector)
1177
+ }
1178
+
1179
+ this.frame = frameLocatorObj
1180
+ this.context = frameLocatorObj
1181
+ this.contextLocator = null
1182
+ this.withinLocator = new Locator(frame)
1183
+ return
907
1184
  }
908
1185
  await this.switchTo(frame)
909
1186
  this.withinLocator = new Locator(frame)
@@ -920,7 +1197,11 @@ class Playwright extends Helper {
920
1197
 
921
1198
  async _withinEnd() {
922
1199
  this.withinLocator = null
923
- this.context = await this.page
1200
+ if (this.page) {
1201
+ this.context = await this.page
1202
+ } else {
1203
+ this.context = null
1204
+ }
924
1205
  this.contextLocator = null
925
1206
  this.frame = null
926
1207
  }
@@ -943,6 +1224,12 @@ class Playwright extends Helper {
943
1224
  if (this.isElectron) {
944
1225
  throw new Error('Cannot open pages inside an Electron container')
945
1226
  }
1227
+
1228
+ // Prevent navigation attempts when browser is being torn down
1229
+ if (!this.isRunning && (!this.browser || !this.browserContext || !this.page)) {
1230
+ throw new Error('Cannot navigate: browser is not running or has been closed')
1231
+ }
1232
+
946
1233
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
947
1234
  url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
948
1235
  this.debug(`Changed URL to base url + relative path: ${url}`)
@@ -955,7 +1242,77 @@ class Playwright extends Helper {
955
1242
  }
956
1243
  }
957
1244
 
958
- await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
1245
+ // Ensure browser is initialized before page operations
1246
+ if (!this.page) {
1247
+ this.debugSection('Auto-initializing', `Browser not started properly. page=${!!this.page}, isRunning=${this.isRunning}, browser=${!!this.browser}, browserContext=${!!this.browserContext}`)
1248
+
1249
+ if (!this.browser) {
1250
+ await this._startBrowser()
1251
+ }
1252
+
1253
+ // Create browser context and page (simplified version of _before logic)
1254
+ if (!this.browserContext) {
1255
+ if (!this.browser) {
1256
+ throw new Error('Browser is not available for context creation. Browser may have been closed.')
1257
+ }
1258
+ const contextOptions = {
1259
+ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors,
1260
+ acceptDownloads: true,
1261
+ ...this.options.emulate,
1262
+ }
1263
+ this.browserContext = await this.browser.newContext(contextOptions)
1264
+ }
1265
+
1266
+ let pages
1267
+ let mainPage
1268
+ try {
1269
+ pages = await this.browserContext.pages()
1270
+ mainPage = pages[0] || (await this.browserContext.newPage())
1271
+ } catch (e) {
1272
+ if (e.message.includes('Target page, context or browser has been closed') || e.message.includes('Browser has been closed')) {
1273
+ throw new Error('Cannot create page: browser context has been closed')
1274
+ }
1275
+ throw e
1276
+ }
1277
+ await this._setPage(mainPage)
1278
+
1279
+ this.debugSection('Auto-initializing', `Completed. page=${!!this.page}, browserContext=${!!this.browserContext}`)
1280
+ }
1281
+
1282
+ // Additional safety check
1283
+ if (!this.page) {
1284
+ throw new Error(`Page is not initialized after auto-initialization. this.page=${this.page}, this.isRunning=${this.isRunning}, this.browser=${!!this.browser}, this.browserContext=${!!this.browserContext}`)
1285
+ }
1286
+
1287
+ try {
1288
+ // Additional validation before navigation
1289
+ if (this.page && this.page.isClosed && this.page.isClosed()) {
1290
+ throw new Error('Cannot navigate: page has been closed')
1291
+ }
1292
+
1293
+ if (this.browserContext) {
1294
+ // Try to check if context is still valid
1295
+ try {
1296
+ await Promise.race([this.browserContext.pages(), new Promise((_, reject) => setTimeout(() => reject(new Error('Context check timeout')), 1000))])
1297
+ } catch (contextError) {
1298
+ throw new Error('Cannot navigate: browser context is invalid or closed')
1299
+ }
1300
+ }
1301
+
1302
+ await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
1303
+ } catch (err) {
1304
+ // Handle terminal navigation errors that shouldn't be retried
1305
+ if (
1306
+ err.message &&
1307
+ (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed') || err.message.includes('Cannot navigate'))
1308
+ ) {
1309
+ // Mark this as a terminal error to prevent retries
1310
+ const terminalError = new Error(err.message)
1311
+ terminalError.isTerminal = true
1312
+ throw terminalError
1313
+ }
1314
+ throw err
1315
+ }
959
1316
 
960
1317
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
961
1318
 
@@ -1111,26 +1468,6 @@ class Playwright extends Helper {
1111
1468
  await this.page.mouse.up()
1112
1469
  }
1113
1470
 
1114
- /**
1115
- * Restart browser with a new context and a new page
1116
- *
1117
- * ```js
1118
- * // Restart browser and use a new timezone
1119
- * I.restartBrowser({ timezoneId: 'America/Phoenix' });
1120
- * // Open URL in a new page in changed timezone
1121
- * I.amOnPage('/');
1122
- * // Restart browser, allow reading/copying of text from/into clipboard in Chrome
1123
- * I.restartBrowser({ permissions: ['clipboard-read', 'clipboard-write'] });
1124
- * ```
1125
- *
1126
- * @param {object} [contextOptions] [Options for browser context](https://playwright.dev/docs/api/class-browser#browser-new-context) when starting new browser
1127
- */
1128
- async restartBrowser(contextOptions) {
1129
- await this._stopBrowser()
1130
- await this._startBrowser()
1131
- await this._createContextPage(contextOptions)
1132
- }
1133
-
1134
1471
  /**
1135
1472
  * {{> refreshPage }}
1136
1473
  */
@@ -1342,8 +1679,7 @@ class Playwright extends Helper {
1342
1679
  *
1343
1680
  */
1344
1681
  async grabWebElements(locator) {
1345
- const elements = await this._locate(locator)
1346
- return elements.map(element => new WebElement(element, this))
1682
+ return this._locate(locator)
1347
1683
  }
1348
1684
 
1349
1685
  /**
@@ -1351,8 +1687,7 @@ class Playwright extends Helper {
1351
1687
  *
1352
1688
  */
1353
1689
  async grabWebElement(locator) {
1354
- const element = await this._locateElement(locator)
1355
- return new WebElement(element, this)
1690
+ return this._locateElement(locator)
1356
1691
  }
1357
1692
 
1358
1693
  /**
@@ -2008,10 +2343,21 @@ class Playwright extends Helper {
2008
2343
  * {{> grabCookie }}
2009
2344
  */
2010
2345
  async grabCookie(name) {
2011
- const cookies = await this.browserContext.cookies()
2012
- if (!name) return cookies
2013
- const cookie = cookies.filter(c => c.name === name)
2014
- if (cookie[0]) return cookie[0]
2346
+ if (!this.browserContext) {
2347
+ throw new Error('Browser context is not available for grabCookie')
2348
+ }
2349
+
2350
+ try {
2351
+ const cookies = await this.browserContext.cookies()
2352
+ if (!name) return cookies
2353
+ const cookie = cookies.filter(c => c.name === name)
2354
+ if (cookie[0]) return cookie[0]
2355
+ } catch (err) {
2356
+ if (err.message.includes('Target page, context or browser has been closed') || err.message.includes('Browser has been closed')) {
2357
+ throw new Error('Cannot grab cookies: browser context has been closed')
2358
+ }
2359
+ throw err
2360
+ }
2015
2361
  }
2016
2362
 
2017
2363
  /**
@@ -2078,9 +2424,28 @@ class Playwright extends Helper {
2078
2424
  *
2079
2425
  */
2080
2426
  async grabTextFrom(locator) {
2081
- locator = this._contextLocator(locator)
2082
- const text = await this.page.textContent(locator)
2083
- assertElementExists(text, locator)
2427
+ const originalLocator = locator
2428
+ const matchedLocator = new Locator(locator)
2429
+
2430
+ if (!matchedLocator.isFuzzy()) {
2431
+ const els = await this._locate(matchedLocator)
2432
+ assertElementExists(els, locator)
2433
+ const text = await els[0].innerText()
2434
+ this.debugSection('Text', text)
2435
+ return text
2436
+ }
2437
+
2438
+ const contextAwareLocator = this._contextLocator(matchedLocator.value)
2439
+ let text
2440
+ try {
2441
+ text = await this.page.textContent(contextAwareLocator)
2442
+ } catch (err) {
2443
+ if (err.message.includes('Timeout') || err.message.includes('exceeded')) {
2444
+ throw new Error(`Element ${new Locator(originalLocator).toString()} was not found by text|CSS|XPath`)
2445
+ }
2446
+ throw err
2447
+ }
2448
+ assertElementExists(text, contextAwareLocator)
2084
2449
  this.debugSection('Text', text)
2085
2450
  return text
2086
2451
  }
@@ -2282,11 +2647,16 @@ class Playwright extends Helper {
2282
2647
  async saveElementScreenshot(locator, fileName) {
2283
2648
  const outputFile = screenshotOutputFolder(fileName)
2284
2649
 
2285
- const res = await this._locateElement(locator)
2286
- assertElementExists(res, locator)
2287
- const elem = res
2288
- this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
2289
- return elem.screenshot({ path: outputFile, type: 'png' })
2650
+ try {
2651
+ const res = await this._locateElement(locator)
2652
+ assertElementExists(res, locator)
2653
+ const elem = res
2654
+ this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
2655
+ return elem.screenshot({ path: outputFile, type: 'png' })
2656
+ } catch (err) {
2657
+ this.debug(`Failed to take element screenshot: ${err.message}`)
2658
+ throw err
2659
+ }
2290
2660
  }
2291
2661
 
2292
2662
  /**
@@ -2298,25 +2668,70 @@ class Playwright extends Helper {
2298
2668
 
2299
2669
  this.debugSection('Screenshot', relativeDir(outputFile))
2300
2670
 
2301
- await this.page.screenshot({
2302
- path: outputFile,
2303
- fullPage: fullPageOption,
2304
- type: 'png',
2305
- })
2671
+ if (!this.page || !this.browser || !this.browserContext) {
2672
+ this.debug(`Cannot take screenshot: page=${!!this.page}, browser=${!!this.browser}, browserContext=${!!this.browserContext}`)
2673
+ return
2674
+ }
2675
+ if (this.page.isClosed && this.page.isClosed()) {
2676
+ this.debug('Cannot take screenshot: page is closed')
2677
+ return
2678
+ }
2306
2679
 
2307
- if (this.activeSessionName) {
2680
+ try {
2681
+ await Promise.race([
2682
+ this.page.screenshot({
2683
+ path: outputFile,
2684
+ fullPage: fullPageOption,
2685
+ type: 'png',
2686
+ }),
2687
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Screenshot timeout')), 5000)),
2688
+ ])
2689
+ } catch (err) {
2690
+ this.debug(`Failed to take screenshot: ${err.message}`)
2691
+
2692
+ this.hasCleanupError = true
2693
+ this.testFailures.push(`Screenshot failed: ${err.message}`)
2694
+
2695
+ if (err.message.includes('closed') || err.message.includes('Protocol error') || err.message.includes('timeout')) {
2696
+ this.debug('Screenshot failed due to browser/page closure or timeout, continuing...')
2697
+ return
2698
+ }
2699
+ throw err
2700
+ }
2701
+
2702
+ // Handle session screenshots for ALL sessions, not just active one
2703
+ if (this.sessionPages && Object.keys(this.sessionPages).length > 0) {
2308
2704
  for (const sessionName in this.sessionPages) {
2309
- const activeSessionPage = this.sessionPages[sessionName]
2705
+ const sessionPage = this.sessionPages[sessionName]
2310
2706
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2311
2707
 
2312
2708
  this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2313
2709
 
2314
- if (activeSessionPage) {
2315
- await activeSessionPage.screenshot({
2316
- path: outputFile,
2317
- fullPage: fullPageOption,
2318
- type: 'png',
2319
- })
2710
+ try {
2711
+ // Add timeout protection for session screenshots
2712
+ await Promise.race([
2713
+ (async () => {
2714
+ if (sessionPage && !sessionPage.isClosed()) {
2715
+ await sessionPage.screenshot({
2716
+ path: outputFile,
2717
+ fullPage: fullPageOption,
2718
+ type: 'png',
2719
+ })
2720
+ } else {
2721
+ this.debug(`Cannot take session screenshot: session page for '${sessionName}' is closed or undefined`)
2722
+ }
2723
+ })(),
2724
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Session screenshot timeout')), 3000)),
2725
+ ])
2726
+ } catch (err) {
2727
+ this.debug(`Failed to take session screenshot for '${sessionName}': ${err.message}`)
2728
+
2729
+ // Track session screenshot failures
2730
+ this.hasCleanupError = true
2731
+ this.testFailures.push(`Session screenshot failed for '${sessionName}': ${err.message}`)
2732
+
2733
+ // Don't throw here - main screenshot was successful and we don't want to hang
2734
+ // Just log and continue
2320
2735
  }
2321
2736
  }
2322
2737
  }
@@ -2380,19 +2795,15 @@ class Playwright extends Helper {
2380
2795
  if (this.options.recordVideo && this.page && this.page.video()) {
2381
2796
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2382
2797
  for (const sessionName in this.sessionPages) {
2383
- if (sessionName === '') continue
2384
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
2798
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
2385
2799
  }
2386
2800
  }
2387
2801
 
2388
2802
  if (this.options.trace) {
2389
2803
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2390
2804
  for (const sessionName in this.sessionPages) {
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`)
2805
+ if (!this.sessionPages[sessionName].context) continue
2806
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
2396
2807
  }
2397
2808
  }
2398
2809
 
@@ -2406,8 +2817,7 @@ class Playwright extends Helper {
2406
2817
  if (this.options.keepVideoForPassedTests) {
2407
2818
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2408
2819
  for (const sessionName of Object.keys(this.sessionPages)) {
2409
- if (sessionName === '') continue
2410
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
2820
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
2411
2821
  }
2412
2822
  } else {
2413
2823
  this.page
@@ -2422,11 +2832,8 @@ class Playwright extends Helper {
2422
2832
  if (this.options.trace) {
2423
2833
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2424
2834
  for (const sessionName in this.sessionPages) {
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`)
2835
+ if (!this.sessionPages[sessionName].context) continue
2836
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
2430
2837
  }
2431
2838
  }
2432
2839
  } else {
@@ -2779,63 +3186,47 @@ class Playwright extends Helper {
2779
3186
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2780
3187
  .first()
2781
3188
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2782
- .catch(e => {
2783
- throw new Error(errorMessage)
2784
- })
2785
3189
  }
2786
3190
 
2787
3191
  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
- })
3192
+ return contextObject.waitForFunction(
3193
+ ([locator, text, $XPath]) => {
3194
+ eval($XPath)
3195
+ const el = $XPath(null, locator)
3196
+ if (!el.length) return false
3197
+ return el[0].innerText.indexOf(text) > -1
3198
+ },
3199
+ [locator.value, text, $XPath.toString()],
3200
+ { timeout: waitTimeout },
3201
+ )
2802
3202
  }
2803
3203
  } catch (e) {
2804
3204
  throw new Error(`${errorMessage}\n${e.message}`)
2805
3205
  }
2806
3206
  }
2807
3207
 
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
3208
  const timeoutGap = waitTimeout + 1000
2811
3209
 
3210
+ // We add basic timeout to make sure we don't wait forever
3211
+ // We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
3212
+ // or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
3213
+ // If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
2812
3214
  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
3215
+ new Promise((_, reject) => {
3216
+ setTimeout(() => reject(errorMessage), waitTimeout)
3217
+ }),
3218
+ this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
3219
+ promiseRetry(
3220
+ async retry => {
3221
+ const textPresent = await contextObject
3222
+ .locator(`:has-text(${JSON.stringify(text)})`)
3223
+ .first()
3224
+ .isVisible()
3225
+ if (!textPresent) retry(errorMessage)
2827
3226
  },
2828
- text,
2829
- { timeout: timeoutGap },
3227
+ { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
2830
3228
  ),
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
- })
3229
+ ])
2839
3230
  }
2840
3231
 
2841
3232
  /**
@@ -2885,8 +3276,13 @@ class Playwright extends Helper {
2885
3276
  }
2886
3277
 
2887
3278
  if (locator >= 0 && locator < childFrames.length) {
2888
- this.context = await this.page.frameLocator('iframe').nth(locator)
2889
- this.contextLocator = locator
3279
+ try {
3280
+ this.context = await Promise.race([this.page.frameLocator('iframe').nth(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Frame locator timeout')), 5000))])
3281
+ this.contextLocator = locator
3282
+ } catch (e) {
3283
+ console.warn('Warning during frame selection:', e.message)
3284
+ throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath')
3285
+ }
2890
3286
  } else {
2891
3287
  throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath')
2892
3288
  }
@@ -2902,16 +3298,25 @@ class Playwright extends Helper {
2902
3298
 
2903
3299
  // iframe by selector
2904
3300
  locator = buildLocatorString(new Locator(locator, 'css'))
2905
- const frame = await this._locateElement(locator)
3301
+
3302
+ let frame
3303
+ try {
3304
+ frame = await Promise.race([this._locateElement(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Locate frame timeout')), 5000))])
3305
+ } catch (e) {
3306
+ console.warn('Warning during frame location:', e.message)
3307
+ frame = null
3308
+ }
2906
3309
 
2907
3310
  if (!frame) {
2908
3311
  throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`)
2909
3312
  }
2910
3313
 
2911
- if (this.frame) {
2912
- this.frame = await this.frame.frameLocator(locator)
2913
- } else {
2914
- this.frame = await this.page.frameLocator(locator)
3314
+ try {
3315
+ // Always create frame locator from page to avoid nested frame paths
3316
+ this.frame = await Promise.race([this.page.frameLocator(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Frame locator timeout')), 5000))])
3317
+ } catch (e) {
3318
+ console.warn('Warning during frame locator creation:', e.message)
3319
+ throw new Error(`Frame ${JSON.stringify(locator)} could not be accessed`)
2915
3320
  }
2916
3321
 
2917
3322
  const contentFrame = this.frame
@@ -2920,8 +3325,14 @@ class Playwright extends Helper {
2920
3325
  this.context = contentFrame
2921
3326
  this.contextLocator = null
2922
3327
  } else {
2923
- this.context = this.page.frame(this.page.frames()[1].name())
2924
- this.contextLocator = locator
3328
+ try {
3329
+ this.context = this.page.frame(this.page.frames()[1].name())
3330
+ this.contextLocator = locator
3331
+ } catch (e) {
3332
+ console.warn('Warning during frame context setup:', e.message)
3333
+ this.context = this.page
3334
+ this.contextLocator = null
3335
+ }
2925
3336
  }
2926
3337
  }
2927
3338
 
@@ -3419,48 +3830,7 @@ class Playwright extends Helper {
3419
3830
  }
3420
3831
  }
3421
3832
 
3422
- module.exports = Playwright
3423
-
3424
- function buildLocatorString(locator) {
3425
- if (locator.isCustom()) {
3426
- return `${locator.type}=${locator.value}`
3427
- }
3428
- if (locator.isXPath()) {
3429
- return `xpath=${locator.value}`
3430
- }
3431
- return locator.simplify()
3432
- }
3433
-
3434
- async function findElements(matcher, locator) {
3435
- if (locator.react) return findReact(matcher, locator)
3436
- if (locator.vue) return findVue(matcher, locator)
3437
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3438
- locator = new Locator(locator, 'css')
3439
-
3440
- return matcher.locator(buildLocatorString(locator)).all()
3441
- }
3442
-
3443
- async function findElement(matcher, locator) {
3444
- if (locator.react) return findReact(matcher, locator)
3445
- if (locator.vue) return findVue(matcher, locator)
3446
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3447
- locator = new Locator(locator, 'css')
3448
-
3449
- return matcher.locator(buildLocatorString(locator)).first()
3450
- }
3451
-
3452
- async function getVisibleElements(elements) {
3453
- const visibleElements = []
3454
- for (const element of elements) {
3455
- if (await element.isVisible()) {
3456
- visibleElements.push(element)
3457
- }
3458
- }
3459
- if (visibleElements.length === 0) {
3460
- return elements
3461
- }
3462
- return visibleElements
3463
- }
3833
+ export default Playwright
3464
3834
 
3465
3835
  async function proceedClick(locator, context = null, options = {}) {
3466
3836
  let matcher = await this._getContext()
@@ -3498,15 +3868,26 @@ async function proceedClick(locator, context = null, options = {}) {
3498
3868
  }
3499
3869
 
3500
3870
  async function findClickable(matcher, locator) {
3501
- if (locator.react) return findReact(matcher, locator)
3502
- if (locator.vue) return findVue(matcher, locator)
3503
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3871
+ const matchedLocator = new Locator(locator)
3504
3872
 
3505
- locator = new Locator(locator)
3506
- if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
3873
+ if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
3507
3874
 
3508
3875
  let els
3509
- const literal = xpathLocator.literal(locator.value)
3876
+ const literal = xpathLocator.literal(matchedLocator.value)
3877
+
3878
+ try {
3879
+ els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
3880
+ if (els.length) return els
3881
+ } catch (err) {
3882
+ // getByRole not supported or failed
3883
+ }
3884
+
3885
+ try {
3886
+ els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
3887
+ if (els.length) return els
3888
+ } catch (err) {
3889
+ // getByRole not supported or failed
3890
+ }
3510
3891
 
3511
3892
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
3512
3893
  if (els.length) return els
@@ -3521,7 +3902,7 @@ async function findClickable(matcher, locator) {
3521
3902
  // Do nothing
3522
3903
  }
3523
3904
 
3524
- return findElements.call(this, matcher, locator.value) // by css or xpath
3905
+ return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
3525
3906
  }
3526
3907
 
3527
3908
  async function proceedSee(assertType, text, context, strict = false) {
@@ -3562,10 +3943,10 @@ async function findCheckable(locator, context) {
3562
3943
 
3563
3944
  const matchedLocator = new Locator(locator)
3564
3945
  if (!matchedLocator.isFuzzy()) {
3565
- return findElements.call(this, contextEl, matchedLocator.simplify())
3946
+ return findElements.call(this, contextEl, matchedLocator)
3566
3947
  }
3567
3948
 
3568
- const literal = xpathLocator.literal(locator)
3949
+ const literal = xpathLocator.literal(matchedLocator.value)
3569
3950
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
3570
3951
  if (els.length) {
3571
3952
  return els
@@ -3574,7 +3955,7 @@ async function findCheckable(locator, context) {
3574
3955
  if (els.length) {
3575
3956
  return els
3576
3957
  }
3577
- return findElements.call(this, contextEl, locator)
3958
+ return findElements.call(this, contextEl, matchedLocator.value)
3578
3959
  }
3579
3960
 
3580
3961
  async function proceedIsChecked(assertType, option) {
@@ -3859,36 +4240,67 @@ async function clickablePoint(el) {
3859
4240
  }
3860
4241
 
3861
4242
  async function refreshContextSession() {
3862
- // close other sessions
4243
+ // close other sessions with timeout protection, but preserve active session contexts
3863
4244
  try {
3864
- const contexts = await this.browser.contexts()
3865
- contexts.shift()
4245
+ const contexts = await Promise.race([this.browser.contexts(), new Promise((_, reject) => setTimeout(() => reject(new Error('Get contexts timeout')), 3000))])
4246
+
4247
+ // Keep the first context (default) and any contexts that belong to active sessions
4248
+ const defaultContext = contexts.shift()
4249
+ const activeSessionContexts = new Set()
4250
+
4251
+ // Identify contexts that are still in use by active sessions
4252
+ if (this.sessionPages) {
4253
+ for (const sessionName in this.sessionPages) {
4254
+ const sessionPage = this.sessionPages[sessionName]
4255
+ if (sessionPage && sessionPage.context) {
4256
+ activeSessionContexts.add(sessionPage.context)
4257
+ }
4258
+ }
4259
+ }
4260
+
4261
+ // Only close contexts that are not in use by active sessions
4262
+ const contextsToClose = contexts.filter(context => !activeSessionContexts.has(context))
3866
4263
 
3867
- await Promise.all(contexts.map(c => c.close()))
4264
+ if (contextsToClose.length > 0) {
4265
+ await Promise.race([Promise.all(contextsToClose.map(c => c.close())), new Promise((_, reject) => setTimeout(() => reject(new Error('Close contexts timeout')), 5000))])
4266
+ }
3868
4267
  } catch (e) {
3869
- console.log(e)
4268
+ console.warn('Warning during context cleanup:', e.message)
3870
4269
  }
3871
4270
 
3872
4271
  if (this.page) {
3873
- const existingPages = await this.browserContext.pages()
3874
- await this._setPage(existingPages[0])
4272
+ try {
4273
+ const existingPages = await this.browserContext.pages()
4274
+ await this._setPage(existingPages[0])
4275
+ } catch (e) {
4276
+ console.warn('Warning during page setup:', e.message)
4277
+ }
3875
4278
  }
3876
4279
 
3877
4280
  if (this.options.keepBrowserState) return
3878
4281
 
3879
4282
  if (!this.options.keepCookies) {
3880
4283
  this.debugSection('Session', 'cleaning cookies and localStorage')
3881
- await this.clearCookie()
4284
+ try {
4285
+ await this.clearCookie()
4286
+ } catch (e) {
4287
+ console.warn('Warning during cookie cleanup:', e.message)
4288
+ }
3882
4289
  }
3883
- const currentUrl = await this.grabCurrentUrl()
3884
4290
 
3885
- if (currentUrl.startsWith('http')) {
3886
- await this.executeScript('localStorage.clear();').catch(err => {
3887
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3888
- })
3889
- await this.executeScript('sessionStorage.clear();').catch(err => {
3890
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
3891
- })
4291
+ try {
4292
+ const currentUrl = await this.grabCurrentUrl()
4293
+
4294
+ if (currentUrl.startsWith('http')) {
4295
+ await this.executeScript('localStorage.clear();').catch(err => {
4296
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4297
+ })
4298
+ await this.executeScript('sessionStorage.clear();').catch(err => {
4299
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4300
+ })
4301
+ }
4302
+ } catch (e) {
4303
+ console.warn('Warning during storage cleanup:', e.message)
3892
4304
  }
3893
4305
  }
3894
4306
 
@@ -3910,18 +4322,9 @@ function saveVideoForPage(page, name) {
3910
4322
  async function saveTraceForContext(context, name) {
3911
4323
  if (!context) return
3912
4324
  if (!context.tracing) return
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
- }
4325
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4326
+ await context.tracing.stop({ path: fileName })
4327
+ return fileName
3925
4328
  }
3926
4329
 
3927
4330
  async function highlightActiveElement(element) {