codeceptjs 3.7.6-beta.4 → 4.0.0-beta.10.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 (191) hide show
  1. package/README.md +1 -3
  2. package/bin/codecept.js +51 -53
  3. package/bin/test-server.js +14 -3
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/actor.js +15 -11
  6. package/lib/ai.js +72 -107
  7. package/lib/assert/empty.js +9 -8
  8. package/lib/assert/equal.js +15 -17
  9. package/lib/assert/error.js +2 -2
  10. package/lib/assert/include.js +9 -11
  11. package/lib/assert/throws.js +1 -1
  12. package/lib/assert/truth.js +8 -5
  13. package/lib/assert.js +18 -18
  14. package/lib/codecept.js +102 -75
  15. package/lib/colorUtils.js +48 -50
  16. package/lib/command/check.js +32 -27
  17. package/lib/command/configMigrate.js +11 -10
  18. package/lib/command/definitions.js +16 -10
  19. package/lib/command/dryRun.js +16 -16
  20. package/lib/command/generate.js +62 -27
  21. package/lib/command/gherkin/init.js +36 -38
  22. package/lib/command/gherkin/snippets.js +14 -14
  23. package/lib/command/gherkin/steps.js +21 -18
  24. package/lib/command/info.js +8 -8
  25. package/lib/command/init.js +36 -29
  26. package/lib/command/interactive.js +11 -10
  27. package/lib/command/list.js +10 -9
  28. package/lib/command/run-multiple/chunk.js +5 -5
  29. package/lib/command/run-multiple/collection.js +5 -5
  30. package/lib/command/run-multiple/run.js +3 -3
  31. package/lib/command/run-multiple.js +16 -13
  32. package/lib/command/run-rerun.js +6 -7
  33. package/lib/command/run-workers.js +24 -9
  34. package/lib/command/run.js +23 -8
  35. package/lib/command/utils.js +20 -18
  36. package/lib/command/workers/runTests.js +197 -114
  37. package/lib/config.js +124 -51
  38. package/lib/container.js +438 -87
  39. package/lib/data/context.js +6 -5
  40. package/lib/data/dataScenarioConfig.js +1 -1
  41. package/lib/data/dataTableArgument.js +1 -1
  42. package/lib/data/table.js +1 -1
  43. package/lib/effects.js +94 -10
  44. package/lib/element/WebElement.js +2 -2
  45. package/lib/els.js +11 -9
  46. package/lib/event.js +11 -10
  47. package/lib/globals.js +141 -0
  48. package/lib/heal.js +12 -12
  49. package/lib/helper/AI.js +11 -11
  50. package/lib/helper/ApiDataFactory.js +50 -19
  51. package/lib/helper/Appium.js +19 -27
  52. package/lib/helper/FileSystem.js +32 -12
  53. package/lib/helper/GraphQL.js +3 -3
  54. package/lib/helper/GraphQLDataFactory.js +4 -4
  55. package/lib/helper/JSONResponse.js +25 -29
  56. package/lib/helper/Mochawesome.js +7 -4
  57. package/lib/helper/Playwright.js +902 -164
  58. package/lib/helper/Puppeteer.js +383 -76
  59. package/lib/helper/REST.js +29 -12
  60. package/lib/helper/WebDriver.js +268 -61
  61. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +18 -9
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
  71. package/lib/helper/extras/Popup.js +1 -1
  72. package/lib/helper/extras/React.js +29 -30
  73. package/lib/helper/network/actions.js +29 -44
  74. package/lib/helper/network/utils.js +76 -83
  75. package/lib/helper/scripts/blurElement.js +6 -6
  76. package/lib/helper/scripts/focusElement.js +6 -6
  77. package/lib/helper/scripts/highlightElement.js +9 -9
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -1
  80. package/lib/history.js +23 -20
  81. package/lib/hooks.js +10 -10
  82. package/lib/html.js +90 -100
  83. package/lib/index.js +48 -21
  84. package/lib/listener/config.js +19 -12
  85. package/lib/listener/emptyRun.js +6 -7
  86. package/lib/listener/enhancedGlobalRetry.js +6 -6
  87. package/lib/listener/exit.js +4 -3
  88. package/lib/listener/globalRetry.js +5 -5
  89. package/lib/listener/globalTimeout.js +30 -14
  90. package/lib/listener/helpers.js +39 -14
  91. package/lib/listener/mocha.js +3 -4
  92. package/lib/listener/result.js +4 -5
  93. package/lib/listener/retryEnhancer.js +3 -3
  94. package/lib/listener/steps.js +8 -7
  95. package/lib/listener/store.js +3 -3
  96. package/lib/locator.js +213 -192
  97. package/lib/mocha/asyncWrapper.js +105 -62
  98. package/lib/mocha/bdd.js +99 -13
  99. package/lib/mocha/cli.js +59 -26
  100. package/lib/mocha/factory.js +78 -19
  101. package/lib/mocha/featureConfig.js +1 -1
  102. package/lib/mocha/gherkin.js +56 -24
  103. package/lib/mocha/hooks.js +12 -3
  104. package/lib/mocha/index.js +13 -4
  105. package/lib/mocha/inject.js +22 -5
  106. package/lib/mocha/scenarioConfig.js +2 -2
  107. package/lib/mocha/suite.js +9 -2
  108. package/lib/mocha/test.js +10 -7
  109. package/lib/mocha/ui.js +28 -18
  110. package/lib/output.js +10 -8
  111. package/lib/parser.js +44 -44
  112. package/lib/pause.js +15 -16
  113. package/lib/plugin/analyze.js +19 -12
  114. package/lib/plugin/auth.js +20 -21
  115. package/lib/plugin/autoDelay.js +12 -8
  116. package/lib/plugin/coverage.js +28 -11
  117. package/lib/plugin/customLocator.js +3 -3
  118. package/lib/plugin/customReporter.js +3 -2
  119. package/lib/plugin/enhancedRetryFailedStep.js +6 -6
  120. package/lib/plugin/heal.js +14 -9
  121. package/lib/plugin/htmlReporter.js +724 -99
  122. package/lib/plugin/pageInfo.js +10 -10
  123. package/lib/plugin/pauseOnFail.js +4 -3
  124. package/lib/plugin/retryFailedStep.js +48 -5
  125. package/lib/plugin/screenshotOnFail.js +75 -37
  126. package/lib/plugin/stepByStepReport.js +14 -14
  127. package/lib/plugin/stepTimeout.js +4 -3
  128. package/lib/plugin/subtitles.js +6 -5
  129. package/lib/recorder.js +33 -14
  130. package/lib/rerun.js +69 -26
  131. package/lib/result.js +4 -4
  132. package/lib/retryCoordinator.js +2 -2
  133. package/lib/secret.js +18 -17
  134. package/lib/session.js +95 -89
  135. package/lib/step/base.js +7 -7
  136. package/lib/step/comment.js +2 -2
  137. package/lib/step/config.js +1 -1
  138. package/lib/step/func.js +3 -3
  139. package/lib/step/helper.js +3 -3
  140. package/lib/step/meta.js +5 -5
  141. package/lib/step/record.js +11 -11
  142. package/lib/step/retry.js +3 -3
  143. package/lib/step/section.js +3 -3
  144. package/lib/step.js +7 -10
  145. package/lib/steps.js +9 -5
  146. package/lib/store.js +1 -1
  147. package/lib/template/heal.js +1 -1
  148. package/lib/template/prompts/generatePageObject.js +31 -0
  149. package/lib/template/prompts/healStep.js +13 -0
  150. package/lib/template/prompts/writeStep.js +9 -0
  151. package/lib/test-server.js +17 -6
  152. package/lib/timeout.js +1 -7
  153. package/lib/transform.js +8 -8
  154. package/lib/translation.js +32 -18
  155. package/lib/utils/mask_data.js +4 -10
  156. package/lib/utils.js +66 -64
  157. package/lib/workerStorage.js +17 -17
  158. package/lib/workers.js +214 -84
  159. package/package.json +41 -37
  160. package/translations/de-DE.js +2 -2
  161. package/translations/fr-FR.js +2 -2
  162. package/translations/index.js +23 -10
  163. package/translations/it-IT.js +2 -2
  164. package/translations/ja-JP.js +2 -2
  165. package/translations/nl-NL.js +2 -2
  166. package/translations/pl-PL.js +2 -2
  167. package/translations/pt-BR.js +2 -2
  168. package/translations/ru-RU.js +2 -2
  169. package/translations/utils.js +4 -3
  170. package/translations/zh-CN.js +2 -2
  171. package/translations/zh-TW.js +2 -2
  172. package/typings/index.d.ts +5 -3
  173. package/typings/promiseBasedTypes.d.ts +4 -0
  174. package/typings/types.d.ts +4 -0
  175. package/lib/helper/Nightmare.js +0 -1486
  176. package/lib/helper/Protractor.js +0 -1840
  177. package/lib/helper/TestCafe.js +0 -1391
  178. package/lib/helper/clientscripts/nightmare.js +0 -213
  179. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  180. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  181. package/lib/plugin/allure.js +0 -15
  182. package/lib/plugin/autoLogin.js +0 -5
  183. package/lib/plugin/commentStep.js +0 -141
  184. package/lib/plugin/eachElement.js +0 -127
  185. package/lib/plugin/fakerTransform.js +0 -49
  186. package/lib/plugin/retryTo.js +0 -16
  187. package/lib/plugin/selenoid.js +0 -364
  188. package/lib/plugin/standardActingHelpers.js +0 -6
  189. package/lib/plugin/tryTo.js +0 -16
  190. package/lib/plugin/wdio.js +0 -247
  191. 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,14 @@ 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 { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
34
+ import WebElement from '../element/WebElement.js'
37
35
 
38
36
  let playwright
39
37
  let perfTiming
@@ -41,14 +39,19 @@ let defaultSelectorEnginesInitialized = false
41
39
  let registeredCustomLocatorStrategies = new Set()
42
40
  let globalCustomLocatorStrategies = new Map()
43
41
 
42
+ // Use global object to track selector registration across workers
43
+ if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
44
+ global.__playwrightSelectorsRegistered = false
45
+ }
46
+
44
47
  const popupStore = new Popup()
45
48
  const consoleLogStore = new Console()
46
49
  const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
47
50
 
48
- const { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts')
49
- const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine')
50
- const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
51
- const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
51
+ import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js'
52
+ import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'
53
+ import { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
54
+ import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
52
55
 
53
56
  const pathSeparator = path.sep
54
57
 
@@ -64,7 +67,6 @@ const pathSeparator = path.sep
64
67
  * @prop {boolean} [show=true] - show browser window.
65
68
  * @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
66
69
  * * '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.
67
- * * 'browser' or **true** - closes browser and opens it again between tests.
68
70
  * * '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
69
71
  * @prop {number} [timeout=1000] - - [timeout](https://playwright.dev/docs/api/class-page#page-set-default-timeout) in ms of all Playwright actions .
70
72
  * @prop {boolean} [disableScreenshots=false] - don't save screenshot on failure.
@@ -330,7 +332,7 @@ class Playwright extends Helper {
330
332
  constructor(config) {
331
333
  super(config)
332
334
 
333
- playwright = requireWithFallback('playwright', 'playwright-core')
335
+ // playwright will be loaded dynamically in _init method
334
336
 
335
337
  // set defaults
336
338
  this.isRemoteBrowser = false
@@ -353,7 +355,20 @@ class Playwright extends Helper {
353
355
  this.recordingWebSocketMessages = false
354
356
  this.recordedWebSocketMessagesAtLeastOnce = false
355
357
  this.cdpSession = null
356
- this.customLocatorStrategies = typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null ? config.customLocatorStrategies : null
358
+
359
+ // Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
360
+ // This can happen in worker threads where config is serialized/deserialized
361
+ let validCustomLocators = null
362
+ if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
363
+ // Check if it's an empty array or object with no function properties
364
+ const entries = Object.entries(config.customLocatorStrategies)
365
+ const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
366
+ if (hasFunctions) {
367
+ validCustomLocators = config.customLocatorStrategies
368
+ }
369
+ }
370
+
371
+ this.customLocatorStrategies = validCustomLocators
357
372
  this._customLocatorsRegistered = false
358
373
 
359
374
  // Add custom locator strategies to global registry for early registration
@@ -363,6 +378,10 @@ class Playwright extends Helper {
363
378
  }
364
379
  }
365
380
 
381
+ // Add test failure tracking to prevent false positives
382
+ this.testFailures = []
383
+ this.hasCleanupError = false
384
+
366
385
  // override defaults with config
367
386
  this._setConfig(config)
368
387
 
@@ -479,20 +498,69 @@ class Playwright extends Helper {
479
498
 
480
499
  static _checkRequirements() {
481
500
  try {
482
- requireWithFallback('playwright', 'playwright-core')
501
+ // In ESM, playwright will be checked via dynamic import in constructor
502
+ // The import will fail at module load time if playwright is missing
503
+ return null
483
504
  } catch (e) {
484
505
  return ['playwright@^1.18']
485
506
  }
486
507
  }
487
508
 
488
509
  async _init() {
510
+ // Load playwright dynamically with fallback
511
+ if (!playwright) {
512
+ try {
513
+ playwright = await import('playwright')
514
+ playwright = playwright.default || playwright
515
+ } catch (e) {
516
+ try {
517
+ playwright = await import('playwright-core')
518
+ playwright = playwright.default || playwright
519
+ } catch (e2) {
520
+ throw new Error('Neither playwright nor playwright-core could be loaded. Please install one of them.')
521
+ }
522
+ }
523
+ }
524
+
525
+ // Ensure custom locators from this instance are in the global registry
526
+ // This is critical for worker threads where globalCustomLocatorStrategies is a new Map
527
+ if (this.customLocatorStrategies) {
528
+ for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
529
+ if (!globalCustomLocatorStrategies.has(strategyName)) {
530
+ globalCustomLocatorStrategies.set(strategyName, strategyFunction)
531
+ }
532
+ }
533
+ }
534
+
489
535
  // register an internal selector engine for reading value property of elements in a selector
490
536
  try {
491
- if (!defaultSelectorEnginesInitialized) {
492
- await playwright.selectors.register('__value', createValueEngine)
493
- await playwright.selectors.register('__disabled', createDisabledEngine)
494
- if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
537
+ // Always wrap in try-catch since selectors might be registered globally across workers
538
+ // Check global flag to avoid re-registration in worker processes
539
+ if (!global.__playwrightSelectorsRegistered) {
540
+ try {
541
+ await playwright.selectors.register('__value', createValueEngine)
542
+ await playwright.selectors.register('__disabled', createDisabledEngine)
543
+ global.__playwrightSelectorsRegistered = true
544
+ defaultSelectorEnginesInitialized = true
545
+ } catch (e) {
546
+ if (!e.message.includes('already registered')) {
547
+ throw e
548
+ }
549
+ // Selector already registered globally by another worker
550
+ global.__playwrightSelectorsRegistered = true
551
+ defaultSelectorEnginesInitialized = true
552
+ }
553
+ } else {
554
+ // Selectors already registered in a worker, skip
495
555
  defaultSelectorEnginesInitialized = true
556
+ this.debugSection('Init', 'Default selector engines already registered globally, skipping')
557
+ }
558
+ if (process.env.testIdAttribute) {
559
+ try {
560
+ await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
561
+ } catch (e) {
562
+ // Ignore if already set
563
+ }
496
564
  }
497
565
 
498
566
  // Register all custom locator strategies from the global registry
@@ -548,15 +616,41 @@ class Playwright extends Helper {
548
616
  }
549
617
 
550
618
  _beforeSuite() {
551
- if ((restartsSession() || restartsContext()) && !this.options.manualStart && !this.isRunning) {
619
+ // Skip browser start in dry-run mode (used by check command)
620
+ if (store.dryRun) {
621
+ this.debugSection('Dry Run', 'Skipping browser start')
622
+ return
623
+ }
624
+
625
+ // Start browser if not manually started and not already running
626
+ // Browser should start in singleton mode (restart: false) or when restart strategy is enabled
627
+ if (!this.options.manualStart && !this.isRunning) {
552
628
  this.debugSection('Session', 'Starting singleton browser session')
553
629
  return this._startBrowser()
554
630
  }
555
631
  }
556
632
 
557
633
  async _before(test) {
634
+ // Skip browser operations in dry-run mode (used by check command)
635
+ if (store.dryRun) {
636
+ this.currentRunningTest = test
637
+ return
638
+ }
639
+
558
640
  this.currentRunningTest = test
559
641
 
642
+ // Reset failure tracking for each test to prevent false positives
643
+ this.hasCleanupError = false
644
+ this.testFailures = []
645
+
646
+ // Reset frame context to ensure clean state for each test
647
+ this.context = this.page
648
+ this.frame = null
649
+ this.contextLocator = null
650
+
651
+ // Clear popup state to ensure clean state for each test
652
+ popupStore.clear()
653
+
560
654
  recorder.retry({
561
655
  retries: test?.opts?.conditionalRetries || 3,
562
656
  when: err => {
@@ -568,8 +662,12 @@ class Playwright extends Helper {
568
662
  },
569
663
  })
570
664
 
571
- if (restartsBrowser() && !this.options.manualStart) await this._startBrowser()
665
+ // Start browser if needed (initial start or browser restart strategy)
572
666
  if (!this.isRunning && !this.options.manualStart) await this._startBrowser()
667
+ else if (restartsBrowser() && !this.options.manualStart) {
668
+ // Browser restart strategy: start browser for each test
669
+ await this._startBrowser()
670
+ }
573
671
 
574
672
  this.isAuthenticated = false
575
673
  if (this.isElectron) {
@@ -606,8 +704,29 @@ class Playwright extends Helper {
606
704
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
607
705
  this.contextOptions = contextOptions
608
706
  if (!this.browserContext || !restartsSession()) {
707
+ if (!this.browser) {
708
+ if (this.options.manualStart) {
709
+ this.debugSection('Manual Start', 'Browser not started - skipping context creation')
710
+ return // Skip context creation when manualStart is true
711
+ } else {
712
+ throw new Error('Browser not started. This should not happen.')
713
+ }
714
+ }
609
715
  this.debugSection('New Session', JSON.stringify(this.contextOptions))
610
- this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
716
+ try {
717
+ this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
718
+ } catch (err) {
719
+ // In worker mode with Playwright 1.x, there's a known issue where newContext() fails
720
+ // with "selector engine already registered" when selectors are registered globally
721
+ // across worker threads. This is safe to retry without ANY custom options.
722
+ if (err.message && err.message.includes('already registered')) {
723
+ this.debugSection('Worker Mode', 'Selector conflict detected, retrying context creation with no options')
724
+ // Create context with NO options to avoid selector conflicts
725
+ this.browserContext = await this.browser.newContext()
726
+ } else {
727
+ throw err
728
+ }
729
+ }
611
730
  }
612
731
  }
613
732
 
@@ -652,8 +771,12 @@ class Playwright extends Helper {
652
771
  popupStore.clear()
653
772
 
654
773
  if (this.isElectron) {
655
- this.browser.close()
656
- this.electronSessions.forEach(session => session.close())
774
+ try {
775
+ this.browser.close()
776
+ this.electronSessions.forEach(session => session.close())
777
+ } catch (e) {
778
+ console.warn('Warning during electron cleanup:', e.message)
779
+ }
657
780
  return
658
781
  }
659
782
 
@@ -662,33 +785,203 @@ class Playwright extends Helper {
662
785
  }
663
786
 
664
787
  if (restartsBrowser()) {
665
- this.isRunning = false
666
- return this._stopBrowser()
788
+ // Close browser completely for restart strategy
789
+ if (this.isRunning) {
790
+ try {
791
+ // Close all pages first to release resources
792
+ if (this.browserContext) {
793
+ const pages = await this.browserContext.pages()
794
+ await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
795
+ }
796
+ // Use timeout to prevent hanging (10s should be enough for browser cleanup)
797
+ await Promise.race([
798
+ this._stopBrowser(),
799
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
800
+ ])
801
+ } catch (e) {
802
+ console.warn('Warning during browser restart in _after:', e.message)
803
+ // Force cleanup even on timeout
804
+ this.browser = null
805
+ this.browserContext = null
806
+ this.isRunning = false
807
+ }
808
+ }
809
+ return
667
810
  }
668
811
 
669
- // close other sessions
812
+ // close other sessions with timeout protection, but only if restartsContext() is true
813
+ if (restartsContext()) {
814
+ try {
815
+ if ((await this.browser)?._type === 'Browser') {
816
+ const contexts = await Promise.race([this.browser.contexts(), new Promise((_, reject) => setTimeout(() => reject(new Error('Get contexts timeout')), 3000))])
817
+ const currentContext = contexts[0]
818
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
819
+ try {
820
+ this.storageState = await currentContext.storageState()
821
+ } catch (e) {
822
+ console.warn('Warning during storage state save:', e.message)
823
+ }
824
+ }
825
+
826
+ await Promise.race([Promise.all(contexts.map(c => c.close())), new Promise((_, reject) => setTimeout(() => reject(new Error('Close contexts timeout')), 5000))])
827
+ }
828
+ } catch (e) {
829
+ console.warn('Warning during context cleanup in _after:', e.message)
830
+ }
831
+ }
832
+
833
+ return this.browser
834
+ }
835
+
836
+ async _afterSuite() {
837
+ // Stop browser after suite completes
838
+ // For restart strategies: stop after each suite
839
+ // For session mode (restart:false): stop after the last suite
840
+ if (this.isRunning) {
841
+ try {
842
+ // Add timeout protection to prevent hanging
843
+ await Promise.race([
844
+ this._stopBrowser(),
845
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
846
+ ])
847
+ } catch (e) {
848
+ console.warn('Warning during suite cleanup:', e.message)
849
+ // Track suite cleanup failures
850
+ this.hasCleanupError = true
851
+ this.testFailures.push(`Suite cleanup failed: ${e.message}`)
852
+ // Force cleanup on timeout
853
+ this.browser = null
854
+ this.browserContext = null
855
+ this.isRunning = false
856
+ } finally {
857
+ this.isRunning = false
858
+ }
859
+ }
860
+
861
+ // Force cleanup of any remaining browser processes
862
+ try {
863
+ if (this.browser && (!this.browser.isConnected || this.browser)) {
864
+ await Promise.race([Promise.resolve(), new Promise(resolve => setTimeout(resolve, 1000))])
865
+ }
866
+ } catch (e) {
867
+ console.warn('Final cleanup warning:', e.message)
868
+ this.hasCleanupError = true
869
+ this.testFailures.push(`Final cleanup failed: ${e.message}`)
870
+ }
871
+
872
+ // Clean up session pages explicitly to prevent hanging references
670
873
  try {
671
- if ((await this.browser)._type === 'Browser') {
672
- const contexts = await this.browser.contexts()
673
- const currentContext = contexts[0]
674
- if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
675
- this.storageState = await currentContext.storageState()
874
+ if (this.sessionPages && Object.keys(this.sessionPages).length > 0) {
875
+ for (const sessionName in this.sessionPages) {
876
+ const sessionPage = this.sessionPages[sessionName]
877
+ if (sessionPage && !sessionPage.isClosed()) {
878
+ try {
879
+ // Remove any remaining event listeners from session pages
880
+ sessionPage.removeAllListeners('dialog')
881
+ sessionPage.removeAllListeners('crash')
882
+ sessionPage.removeAllListeners('close')
883
+ sessionPage.removeAllListeners('error')
884
+ await sessionPage.close()
885
+ } catch (e) {
886
+ console.warn(`Warning closing session page ${sessionName}:`, e.message)
887
+ }
888
+ }
676
889
  }
890
+ this.sessionPages = {} // Clear the session pages object
891
+ this.activeSessionName = '' // Reset active session name
892
+ }
893
+ } catch (e) {
894
+ console.warn('Session pages cleanup warning:', e.message)
895
+ this.hasCleanupError = true
896
+ this.testFailures.push(`Session cleanup failed: ${e.message}`)
897
+ }
677
898
 
678
- await Promise.all(contexts.map(c => c.close()))
899
+ // Clear any lingering DOM timeouts by executing cleanup in browser context
900
+ try {
901
+ if (this.page && !this.page.isClosed()) {
902
+ await this.page
903
+ .evaluate(() => {
904
+ // Clear any running highlight timeouts by clearing a range of timeout IDs
905
+ for (let i = 1; i <= 1000; i++) {
906
+ clearTimeout(i)
907
+ }
908
+ })
909
+ .catch(() => {
910
+ // Ignore errors if execution context is destroyed (e.g., due to navigation)
911
+ })
679
912
  }
680
913
  } catch (e) {
681
- console.log(e)
914
+ // Only log if it's not an execution context error
915
+ if (!e.message.includes('Execution context was destroyed')) {
916
+ console.warn('DOM timeout cleanup warning:', e.message)
917
+ this.hasCleanupError = true
918
+ this.testFailures.push(`DOM cleanup failed: ${e.message}`)
919
+ }
682
920
  }
683
921
 
684
- // await this.closeOtherTabs();
685
- return this.browser
922
+ // If we have cleanup errors, throw to fail the test suite
923
+ if (this.hasCleanupError && this.testFailures.length > 0) {
924
+ const errorMessage = `Test suite cleanup failed: ${this.testFailures.join('; ')}`
925
+ console.error(errorMessage)
926
+ throw new Error(errorMessage)
927
+ }
686
928
  }
687
929
 
688
- _afterSuite() {}
689
-
690
930
  async _finishTest() {
691
- if ((restartsSession() || restartsContext()) && this.isRunning) return this._stopBrowser()
931
+ if ((restartsSession() || restartsContext() || restartsBrowser()) && this.isRunning) {
932
+ try {
933
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Test finish timeout')), 10000))])
934
+ } catch (e) {
935
+ console.warn('Warning during test finish cleanup:', e.message)
936
+ // Track cleanup failures to prevent false positives
937
+ this.hasCleanupError = true
938
+ this.testFailures.push(`Test finish cleanup failed: ${e.message}`)
939
+
940
+ this.isRunning = false
941
+ // Set flags to prevent further operations after cleanup failure
942
+ this.page = null
943
+ this.browserContext = null
944
+ this.browser = null
945
+
946
+ // Propagate the error to fail the test properly
947
+ throw new Error(`Test cleanup failed: ${e.message}`)
948
+ }
949
+ }
950
+ }
951
+
952
+ async _cleanup() {
953
+ // Final cleanup when test run completes
954
+ if (this.isRunning) {
955
+ try {
956
+ // Add timeout protection to prevent hanging
957
+ await Promise.race([
958
+ this._stopBrowser(),
959
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
960
+ ])
961
+ } catch (e) {
962
+ console.warn('Warning during final cleanup:', e.message)
963
+ // Force cleanup on timeout
964
+ this.browser = null
965
+ this.browserContext = null
966
+ this.isRunning = false
967
+ }
968
+ } else {
969
+ // Check if we still have a browser object despite isRunning being false
970
+ if (this.browser) {
971
+ try {
972
+ // Add timeout protection to prevent hanging
973
+ await Promise.race([
974
+ this._stopBrowser(),
975
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
976
+ ])
977
+ } catch (e) {
978
+ console.warn('Warning during forced cleanup:', e.message)
979
+ // Force cleanup on timeout
980
+ this.browser = null
981
+ this.browserContext = null
982
+ }
983
+ }
984
+ }
692
985
  }
693
986
 
694
987
  _session() {
@@ -707,13 +1000,20 @@ class Playwright extends Helper {
707
1000
  page = await browser.firstWindow()
708
1001
  } else {
709
1002
  try {
710
- browserContext = await this.browser.newContext(Object.assign(this.contextOptions, config))
711
- page = await browserContext.newPage()
1003
+ // Check if browser is still available before creating context
1004
+ if (!this.browser) {
1005
+ throw new Error('Browser is not available for session context creation')
1006
+ }
1007
+ browserContext = await Promise.race([this.browser.newContext(Object.assign(this.contextOptions, config)), new Promise((_, reject) => setTimeout(() => reject(new Error('New context timeout')), 10000))])
1008
+ page = await Promise.race([browserContext.newPage(), new Promise((_, reject) => setTimeout(() => reject(new Error('New page timeout')), 5000))])
712
1009
  } catch (e) {
1010
+ console.warn('Warning during context creation:', e.message)
713
1011
  if (this.playwrightOptions.userDataDir) {
714
1012
  browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
715
1013
  this.browser = browserContext
716
1014
  page = await browserContext.pages()[0]
1015
+ } else {
1016
+ throw e
717
1017
  }
718
1018
  }
719
1019
  }
@@ -744,8 +1044,28 @@ class Playwright extends Helper {
744
1044
  } else {
745
1045
  this.activeSessionName = session
746
1046
  }
747
- const existingPages = await this.browserContext.pages()
748
- await this._setPage(existingPages[0])
1047
+
1048
+ // Safety check: ensure browserContext exists before calling pages()
1049
+ if (!this.browserContext) {
1050
+ this.debug('Cannot restore session vars: browserContext is undefined')
1051
+ return
1052
+ }
1053
+
1054
+ try {
1055
+ const existingPages = await this.browserContext.pages()
1056
+ if (existingPages && existingPages.length > 0) {
1057
+ await this._setPage(existingPages[0])
1058
+ // Reset context-related variables to ensure clean state after session
1059
+ this.context = await this.page
1060
+ this.contextLocator = null
1061
+ this.frame = null
1062
+ } else {
1063
+ this.debug('Cannot restore session vars: no pages available')
1064
+ }
1065
+ } catch (err) {
1066
+ this.debug(`Failed to restore session vars: ${err.message}`)
1067
+ return
1068
+ }
749
1069
 
750
1070
  return this._waitForAction()
751
1071
  },
@@ -831,21 +1151,46 @@ class Playwright extends Helper {
831
1151
  * @param {object} page page to set
832
1152
  */
833
1153
  async _setPage(page) {
1154
+ // Clean up previous page event listeners
1155
+ if (this.page && this.page !== page) {
1156
+ try {
1157
+ this.page.removeAllListeners('crash')
1158
+ this.page.removeAllListeners('dialog')
1159
+ this.page.removeAllListeners('load')
1160
+ this.page.removeAllListeners('console')
1161
+ this.page.removeAllListeners('requestfinished')
1162
+ } catch (e) {
1163
+ console.warn('Warning cleaning previous page listeners:', e.message)
1164
+ }
1165
+ }
1166
+
834
1167
  page = await page
835
1168
  this._addPopupListener(page)
836
1169
  this.page = page
837
1170
  if (!page) return
838
- this.browserContext.setDefaultTimeout(0)
839
- page.setDefaultNavigationTimeout(this.options.getPageTimeout)
840
- page.setDefaultTimeout(this.options.timeout)
841
1171
 
842
- page.on('crash', async () => {
843
- console.log('ERROR: Page has crashed, closing page!')
844
- await page.close()
845
- })
846
- this.context = await this.page
847
- this.contextLocator = null
848
- await page.bringToFront()
1172
+ try {
1173
+ this.browserContext.setDefaultTimeout(0)
1174
+ page.setDefaultNavigationTimeout(this.options.getPageTimeout)
1175
+ page.setDefaultTimeout(this.options.timeout)
1176
+
1177
+ page.on('crash', async () => {
1178
+ console.log('ERROR: Page has crashed, closing page!')
1179
+ try {
1180
+ await page.close()
1181
+ } catch (e) {
1182
+ console.warn('Warning during crashed page cleanup:', e.message)
1183
+ }
1184
+ })
1185
+
1186
+ this.context = await this.page
1187
+ this.contextLocator = null
1188
+ await page.bringToFront()
1189
+ } catch (e) {
1190
+ console.warn('Warning during page setup:', e.message)
1191
+ this.context = await this.page
1192
+ this.contextLocator = null
1193
+ }
849
1194
  }
850
1195
 
851
1196
  /**
@@ -903,7 +1248,10 @@ class Playwright extends Helper {
903
1248
 
904
1249
  async _startBrowser() {
905
1250
  // Ensure custom locator strategies are registered before browser launch
906
- await this._init()
1251
+ // Only init once globally to avoid selector re-registration in workers
1252
+ if (!defaultSelectorEnginesInitialized) {
1253
+ await this._init()
1254
+ }
907
1255
 
908
1256
  if (this.isElectron) {
909
1257
  this.browser = await playwright._electron.launch(this.playwrightOptions)
@@ -970,6 +1318,9 @@ class Playwright extends Helper {
970
1318
  * @param {object} [contextOptions] See https://playwright.dev/docs/api/class-browser#browser-new-context
971
1319
  */
972
1320
  async _createContextPage(contextOptions) {
1321
+ if (!this.browser) {
1322
+ throw new Error('Browser not started. Call _startBrowser() first or disable manualStart option.')
1323
+ }
973
1324
  this.browserContext = await this.browser.newContext(contextOptions)
974
1325
 
975
1326
  // Register custom locator strategies for this context
@@ -1039,9 +1390,42 @@ class Playwright extends Helper {
1039
1390
  this.context = null
1040
1391
  this.frame = null
1041
1392
  popupStore.clear()
1042
- if (this.options.recordHar) await this.browserContext.close()
1393
+
1394
+ // Remove all event listeners to prevent hanging
1395
+ if (this.browser) {
1396
+ try {
1397
+ this.browser.removeAllListeners()
1398
+ } catch (e) {
1399
+ // Ignore errors if browser is already closed
1400
+ }
1401
+ }
1402
+
1403
+ if (this.options.recordHar && this.browserContext) {
1404
+ try {
1405
+ await this.browserContext.close()
1406
+ } catch (e) {
1407
+ // Ignore errors if context is already closed
1408
+ }
1409
+ }
1043
1410
  this.browserContext = null
1044
- await this.browser.close()
1411
+
1412
+ if (this.browser) {
1413
+ try {
1414
+ // Add timeout to prevent browser.close() from hanging indefinitely
1415
+ await Promise.race([
1416
+ this.browser.close(),
1417
+ new Promise((_, reject) =>
1418
+ setTimeout(() => reject(new Error('Browser close timeout')), 5000)
1419
+ )
1420
+ ])
1421
+ } catch (e) {
1422
+ // Ignore errors if browser is already closed or timeout
1423
+ if (!e.message?.includes('Browser close timeout')) {
1424
+ // Non-timeout error, can be ignored as well
1425
+ }
1426
+ // Force cleanup even on error
1427
+ }
1428
+ }
1045
1429
  this.browser = null
1046
1430
  this.isRunning = false
1047
1431
  }
@@ -1060,8 +1444,21 @@ class Playwright extends Helper {
1060
1444
 
1061
1445
  if (frame) {
1062
1446
  if (Array.isArray(frame)) {
1447
+ // For nested frames, build the complete frame path
1063
1448
  await this.switchTo(null)
1064
- return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve())
1449
+
1450
+ // Build nested frame locator from page
1451
+ let frameLocatorObj = this.page
1452
+ for (const frameSelector of frame) {
1453
+ const selector = buildLocatorString(new Locator(frameSelector, 'css'))
1454
+ frameLocatorObj = frameLocatorObj.frameLocator(selector)
1455
+ }
1456
+
1457
+ this.frame = frameLocatorObj
1458
+ this.context = frameLocatorObj
1459
+ this.contextLocator = null
1460
+ this.withinLocator = new Locator(frame)
1461
+ return
1065
1462
  }
1066
1463
  await this.switchTo(frame)
1067
1464
  this.withinLocator = new Locator(frame)
@@ -1078,7 +1475,11 @@ class Playwright extends Helper {
1078
1475
 
1079
1476
  async _withinEnd() {
1080
1477
  this.withinLocator = null
1081
- this.context = await this.page
1478
+ if (this.page) {
1479
+ this.context = await this.page
1480
+ } else {
1481
+ this.context = null
1482
+ }
1082
1483
  this.contextLocator = null
1083
1484
  this.frame = null
1084
1485
  }
@@ -1101,6 +1502,13 @@ class Playwright extends Helper {
1101
1502
  if (this.isElectron) {
1102
1503
  throw new Error('Cannot open pages inside an Electron container')
1103
1504
  }
1505
+
1506
+ // Prevent navigation attempts only when manual start is enabled and browser is not running
1507
+ // Allow auto-initialization for normal operation (e.g., when using BROWSER_RESTART=browser)
1508
+ if (!this.isRunning && this.options.manualStart && (!this.browser || !this.browserContext || !this.page)) {
1509
+ throw new Error('Cannot navigate: browser is not running or has been closed')
1510
+ }
1511
+
1104
1512
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
1105
1513
  url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
1106
1514
  this.debug(`Changed URL to base url + relative path: ${url}`)
@@ -1113,7 +1521,91 @@ class Playwright extends Helper {
1113
1521
  }
1114
1522
  }
1115
1523
 
1116
- await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
1524
+ // Ensure browser is initialized before page operations
1525
+ if (!this.page) {
1526
+ this.debugSection('Auto-initializing', `Browser not started properly. page=${!!this.page}, isRunning=${this.isRunning}, browser=${!!this.browser}, browserContext=${!!this.browserContext}`)
1527
+
1528
+ if (!this.browser) {
1529
+ await this._startBrowser()
1530
+ }
1531
+
1532
+ // Create browser context and page (simplified version of _before logic)
1533
+ if (!this.browserContext) {
1534
+ if (!this.browser) {
1535
+ throw new Error('Browser is not available for context creation. Browser may have been closed.')
1536
+ }
1537
+ const contextOptions = {
1538
+ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors,
1539
+ acceptDownloads: true,
1540
+ ...this.options.emulate,
1541
+ }
1542
+
1543
+ try {
1544
+ this.browserContext = await this.browser.newContext(contextOptions)
1545
+ } catch (err) {
1546
+ // In worker mode with Playwright 1.x, there's a known issue where newContext() fails
1547
+ // with "selector engine already registered" when selectors are registered globally
1548
+ // across worker threads. This is safe to retry without ANY custom options.
1549
+ if (err.message && err.message.includes('already registered')) {
1550
+ this.debugSection('Worker Mode', 'Selector conflict in amOnPage, retrying with empty options')
1551
+ // Create context with NO options to avoid selector conflicts
1552
+ this.browserContext = await this.browser.newContext()
1553
+ } else {
1554
+ throw err
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ let pages
1560
+ let mainPage
1561
+ try {
1562
+ pages = await this.browserContext.pages()
1563
+ mainPage = pages[0] || (await this.browserContext.newPage())
1564
+ } catch (e) {
1565
+ if (e.message.includes('Target page, context or browser has been closed') || e.message.includes('Browser has been closed')) {
1566
+ throw new Error('Cannot create page: browser context has been closed')
1567
+ }
1568
+ throw e
1569
+ }
1570
+ await this._setPage(mainPage)
1571
+
1572
+ this.debugSection('Auto-initializing', `Completed. page=${!!this.page}, browserContext=${!!this.browserContext}`)
1573
+ }
1574
+
1575
+ // Additional safety check
1576
+ if (!this.page) {
1577
+ 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}`)
1578
+ }
1579
+
1580
+ try {
1581
+ // Additional validation before navigation
1582
+ if (this.page && this.page.isClosed && this.page.isClosed()) {
1583
+ throw new Error('Cannot navigate: page has been closed')
1584
+ }
1585
+
1586
+ if (this.browserContext) {
1587
+ // Try to check if context is still valid
1588
+ try {
1589
+ await Promise.race([this.browserContext.pages(), new Promise((_, reject) => setTimeout(() => reject(new Error('Context check timeout')), 1000))])
1590
+ } catch (contextError) {
1591
+ throw new Error('Cannot navigate: browser context is invalid or closed')
1592
+ }
1593
+ }
1594
+
1595
+ await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
1596
+ } catch (err) {
1597
+ // Handle terminal navigation errors that shouldn't be retried
1598
+ if (
1599
+ err.message &&
1600
+ (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'))
1601
+ ) {
1602
+ // Mark this as a terminal error to prevent retries
1603
+ const terminalError = new Error(err.message)
1604
+ terminalError.isTerminal = true
1605
+ throw terminalError
1606
+ }
1607
+ throw err
1608
+ }
1117
1609
 
1118
1610
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
1119
1611
 
@@ -1251,44 +1743,25 @@ class Playwright extends Helper {
1251
1743
  async dragAndDrop(srcElement, destElement, options) {
1252
1744
  const src = new Locator(srcElement)
1253
1745
  const dst = new Locator(destElement)
1746
+ const context = await this._getContext()
1254
1747
 
1255
1748
  if (options) {
1256
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options)
1749
+ return context.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options)
1257
1750
  }
1258
1751
 
1259
1752
  const _smallWaitInMs = 600
1260
- await this.page.locator(buildLocatorString(src)).hover()
1753
+ await context.locator(buildLocatorString(src)).hover()
1261
1754
  await this.page.mouse.down()
1262
1755
  await this.page.waitForTimeout(_smallWaitInMs)
1263
1756
 
1264
- const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox()
1757
+ const destElBox = await context.locator(buildLocatorString(dst)).boundingBox()
1265
1758
 
1266
1759
  await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2)
1267
- await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } })
1760
+ await context.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } })
1268
1761
  await this.page.waitForTimeout(_smallWaitInMs)
1269
1762
  await this.page.mouse.up()
1270
1763
  }
1271
1764
 
1272
- /**
1273
- * Restart browser with a new context and a new page
1274
- *
1275
- * ```js
1276
- * // Restart browser and use a new timezone
1277
- * I.restartBrowser({ timezoneId: 'America/Phoenix' });
1278
- * // Open URL in a new page in changed timezone
1279
- * I.amOnPage('/');
1280
- * // Restart browser, allow reading/copying of text from/into clipboard in Chrome
1281
- * I.restartBrowser({ permissions: ['clipboard-read', 'clipboard-write'] });
1282
- * ```
1283
- *
1284
- * @param {object} [contextOptions] [Options for browser context](https://playwright.dev/docs/api/class-browser#browser-new-context) when starting new browser
1285
- */
1286
- async restartBrowser(contextOptions) {
1287
- await this._stopBrowser()
1288
- await this._startBrowser()
1289
- await this._createContextPage(contextOptions)
1290
- }
1291
-
1292
1765
  /**
1293
1766
  * {{> refreshPage }}
1294
1767
  */
@@ -1734,7 +2207,7 @@ class Playwright extends Helper {
1734
2207
  * ```
1735
2208
  *
1736
2209
  */
1737
- async click(locator, context = null, options = {}) {
2210
+ async click(locator = '//body', context = null, options = {}) {
1738
2211
  return proceedClick.call(this, locator, context, options)
1739
2212
  }
1740
2213
 
@@ -1768,6 +2241,49 @@ class Playwright extends Helper {
1768
2241
  return proceedClick.call(this, locator, context, { button: 'right' })
1769
2242
  }
1770
2243
 
2244
+ /**
2245
+ * Performs click at specific coordinates.
2246
+ * If locator is provided, the coordinates are relative to the element.
2247
+ * If locator is not provided, the coordinates are global page coordinates.
2248
+ *
2249
+ * ```js
2250
+ * // Click at global coordinates (100, 200)
2251
+ * I.clickXY(100, 200);
2252
+ *
2253
+ * // Click at coordinates (50, 30) relative to element
2254
+ * I.clickXY('#someElement', 50, 30);
2255
+ * ```
2256
+ *
2257
+ * @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
2258
+ * @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
2259
+ * @param {number} [y] Y coordinate relative to element.
2260
+ * @returns {Promise<void>}
2261
+ */
2262
+ async clickXY(locator, x, y) {
2263
+ // If locator is a number, treat it as global X coordinate
2264
+ if (typeof locator === 'number') {
2265
+ const globalX = locator
2266
+ const globalY = x
2267
+ await this.page.mouse.click(globalX, globalY)
2268
+ return this._waitForAction()
2269
+ }
2270
+
2271
+ // Locator is provided, click relative to element
2272
+ const el = await this._locateElement(locator)
2273
+ assertElementExists(el, locator, 'Element to click')
2274
+
2275
+ const box = await el.boundingBox()
2276
+ if (!box) {
2277
+ throw new Error(`Element ${locator} is not visible or has no bounding box`)
2278
+ }
2279
+
2280
+ const absoluteX = box.x + x
2281
+ const absoluteY = box.y + y
2282
+
2283
+ await this.page.mouse.click(absoluteX, absoluteY)
2284
+ return this._waitForAction()
2285
+ }
2286
+
1771
2287
  /**
1772
2288
  *
1773
2289
  * [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-check) for check available as 3rd argument.
@@ -1876,11 +2392,15 @@ class Playwright extends Helper {
1876
2392
  * {{> type }}
1877
2393
  */
1878
2394
  async type(keys, delay = null) {
2395
+ // Always use page.keyboard.type for any string (including single character and national characters).
1879
2396
  if (!Array.isArray(keys)) {
1880
2397
  keys = keys.toString()
1881
- keys = keys.split('')
2398
+ const typeDelay = typeof delay === 'number' ? delay : this.options.pressKeyDelay
2399
+ await this.page.keyboard.type(keys, { delay: typeDelay })
2400
+ return
1882
2401
  }
1883
2402
 
2403
+ // For array input, treat each as a key press to keep working combinations such as ['Control', 'A'] or ['T', 'e', 's', 't'].
1884
2404
  for (const key of keys) {
1885
2405
  await this.page.keyboard.press(key)
1886
2406
  if (delay) await this.wait(delay / 1000)
@@ -2166,10 +2686,21 @@ class Playwright extends Helper {
2166
2686
  * {{> grabCookie }}
2167
2687
  */
2168
2688
  async grabCookie(name) {
2169
- const cookies = await this.browserContext.cookies()
2170
- if (!name) return cookies
2171
- const cookie = cookies.filter(c => c.name === name)
2172
- if (cookie[0]) return cookie[0]
2689
+ if (!this.browserContext) {
2690
+ throw new Error('Browser context is not available for grabCookie')
2691
+ }
2692
+
2693
+ try {
2694
+ const cookies = await this.browserContext.cookies()
2695
+ if (!name) return cookies
2696
+ const cookie = cookies.filter(c => c.name === name)
2697
+ if (cookie[0]) return cookie[0]
2698
+ } catch (err) {
2699
+ if (err.message.includes('Target page, context or browser has been closed') || err.message.includes('Browser has been closed')) {
2700
+ throw new Error('Cannot grab cookies: browser context has been closed')
2701
+ }
2702
+ throw err
2703
+ }
2173
2704
  }
2174
2705
 
2175
2706
  /**
@@ -2274,6 +2805,17 @@ class Playwright extends Helper {
2274
2805
  *
2275
2806
  */
2276
2807
  async grabTextFrom(locator) {
2808
+ // Handle role locators with text/exact options
2809
+ if (isRoleLocatorObject(locator)) {
2810
+ const elements = await handleRoleLocator(this.page, locator)
2811
+ if (elements && elements.length > 0) {
2812
+ const text = await elements[0].textContent()
2813
+ assertElementExists(text, JSON.stringify(locator))
2814
+ this.debugSection('Text', text)
2815
+ return text
2816
+ }
2817
+ }
2818
+
2277
2819
  const locatorObj = new Locator(locator, 'css')
2278
2820
 
2279
2821
  if (locatorObj.isCustom()) {
@@ -2288,10 +2830,18 @@ class Playwright extends Helper {
2288
2830
  return text
2289
2831
  } else {
2290
2832
  locator = this._contextLocator(locator)
2291
- const text = await this.page.textContent(locator)
2292
- assertElementExists(text, locator)
2293
- this.debugSection('Text', text)
2294
- return text
2833
+ try {
2834
+ const text = await this.page.textContent(locator)
2835
+ assertElementExists(text, locator)
2836
+ this.debugSection('Text', text)
2837
+ return text
2838
+ } catch (error) {
2839
+ // Convert Playwright timeout errors to ElementNotFound for consistency
2840
+ if (error.message && error.message.includes('Timeout')) {
2841
+ throw new ElementNotFound(locator, 'text')
2842
+ }
2843
+ throw error
2844
+ }
2295
2845
  }
2296
2846
  }
2297
2847
 
@@ -2480,6 +3030,33 @@ class Playwright extends Helper {
2480
3030
  return array
2481
3031
  }
2482
3032
 
3033
+ /**
3034
+ * Retrieves the ARIA snapshot for an element using Playwright's [`locator.ariaSnapshot`](https://playwright.dev/docs/api/class-locator#locator-aria-snapshot).
3035
+ * This method returns a YAML representation of the accessibility tree that can be used for assertions.
3036
+ * If no locator is provided, it captures the snapshot of the entire page body.
3037
+ *
3038
+ * ```js
3039
+ * const snapshot = await I.grabAriaSnapshot();
3040
+ * expect(snapshot).toContain('heading "Sign up"');
3041
+ *
3042
+ * const formSnapshot = await I.grabAriaSnapshot('#login-form');
3043
+ * expect(formSnapshot).toContain('textbox "Email"');
3044
+ * ```
3045
+ *
3046
+ * [Learn more about ARIA snapshots](https://playwright.dev/docs/aria-snapshots)
3047
+ *
3048
+ * @param {string|object} [locator='//body'] element located by CSS|XPath|strict locator. Defaults to body element.
3049
+ * @return {Promise<string>} YAML representation of the accessibility tree
3050
+ */
3051
+ async grabAriaSnapshot(locator = '//body') {
3052
+ const matchedLocator = new Locator(locator)
3053
+ const els = await this._locate(matchedLocator)
3054
+ assertElementExists(els, locator)
3055
+ const snapshot = await els[0].ariaSnapshot()
3056
+ this.debugSection('Aria Snapshot', snapshot)
3057
+ return snapshot
3058
+ }
3059
+
2483
3060
  /**
2484
3061
  * {{> saveElementScreenshot }}
2485
3062
  *
@@ -2502,25 +3079,70 @@ class Playwright extends Helper {
2502
3079
 
2503
3080
  this.debugSection('Screenshot', relativeDir(outputFile))
2504
3081
 
2505
- await this.page.screenshot({
2506
- path: outputFile,
2507
- fullPage: fullPageOption,
2508
- type: 'png',
2509
- })
3082
+ if (!this.page || !this.browser || !this.browserContext) {
3083
+ this.debug(`Cannot take screenshot: page=${!!this.page}, browser=${!!this.browser}, browserContext=${!!this.browserContext}`)
3084
+ return
3085
+ }
3086
+ if (this.page.isClosed && this.page.isClosed()) {
3087
+ this.debug('Cannot take screenshot: page is closed')
3088
+ return
3089
+ }
3090
+
3091
+ try {
3092
+ await Promise.race([
3093
+ this.page.screenshot({
3094
+ path: outputFile,
3095
+ fullPage: fullPageOption,
3096
+ type: 'png',
3097
+ }),
3098
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Screenshot timeout')), 5000)),
3099
+ ])
3100
+ } catch (err) {
3101
+ this.debug(`Failed to take screenshot: ${err.message}`)
3102
+
3103
+ this.hasCleanupError = true
3104
+ this.testFailures.push(`Screenshot failed: ${err.message}`)
3105
+
3106
+ if (err.message.includes('closed') || err.message.includes('Protocol error') || err.message.includes('timeout')) {
3107
+ this.debug('Screenshot failed due to browser/page closure or timeout, continuing...')
3108
+ return
3109
+ }
3110
+ throw err
3111
+ }
2510
3112
 
2511
- if (this.activeSessionName) {
3113
+ // Handle session screenshots for ALL sessions, not just active one
3114
+ if (this.sessionPages && Object.keys(this.sessionPages).length > 0) {
2512
3115
  for (const sessionName in this.sessionPages) {
2513
- const activeSessionPage = this.sessionPages[sessionName]
3116
+ const sessionPage = this.sessionPages[sessionName]
2514
3117
  outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
2515
3118
 
2516
3119
  this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
2517
3120
 
2518
- if (activeSessionPage) {
2519
- await activeSessionPage.screenshot({
2520
- path: outputFile,
2521
- fullPage: fullPageOption,
2522
- type: 'png',
2523
- })
3121
+ try {
3122
+ // Add timeout protection for session screenshots
3123
+ await Promise.race([
3124
+ (async () => {
3125
+ if (sessionPage && !sessionPage.isClosed()) {
3126
+ await sessionPage.screenshot({
3127
+ path: outputFile,
3128
+ fullPage: fullPageOption,
3129
+ type: 'png',
3130
+ })
3131
+ } else {
3132
+ this.debug(`Cannot take session screenshot: session page for '${sessionName}' is closed or undefined`)
3133
+ }
3134
+ })(),
3135
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Session screenshot timeout')), 3000)),
3136
+ ])
3137
+ } catch (err) {
3138
+ this.debug(`Failed to take session screenshot for '${sessionName}': ${err.message}`)
3139
+
3140
+ // Track session screenshot failures
3141
+ this.hasCleanupError = true
3142
+ this.testFailures.push(`Session screenshot failed for '${sessionName}': ${err.message}`)
3143
+
3144
+ // Don't throw here - main screenshot was successful and we don't want to hang
3145
+ // Just log and continue
2524
3146
  }
2525
3147
  }
2526
3148
  }
@@ -3130,8 +3752,13 @@ class Playwright extends Helper {
3130
3752
  }
3131
3753
 
3132
3754
  if (locator >= 0 && locator < childFrames.length) {
3133
- this.context = await this.page.frameLocator('iframe').nth(locator)
3134
- this.contextLocator = locator
3755
+ try {
3756
+ this.context = await Promise.race([this.page.frameLocator('iframe').nth(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Frame locator timeout')), 5000))])
3757
+ this.contextLocator = locator
3758
+ } catch (e) {
3759
+ console.warn('Warning during frame selection:', e.message)
3760
+ throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath')
3761
+ }
3135
3762
  } else {
3136
3763
  throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath')
3137
3764
  }
@@ -3147,16 +3774,25 @@ class Playwright extends Helper {
3147
3774
 
3148
3775
  // iframe by selector
3149
3776
  locator = buildLocatorString(new Locator(locator, 'css'))
3150
- const frame = await this._locateElement(locator)
3777
+
3778
+ let frame
3779
+ try {
3780
+ frame = await Promise.race([this._locateElement(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Locate frame timeout')), 5000))])
3781
+ } catch (e) {
3782
+ console.warn('Warning during frame location:', e.message)
3783
+ frame = null
3784
+ }
3151
3785
 
3152
3786
  if (!frame) {
3153
3787
  throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`)
3154
3788
  }
3155
3789
 
3156
- if (this.frame) {
3157
- this.frame = await this.frame.frameLocator(locator)
3158
- } else {
3159
- this.frame = await this.page.frameLocator(locator)
3790
+ try {
3791
+ // Always create frame locator from page to avoid nested frame paths
3792
+ this.frame = await Promise.race([this.page.frameLocator(locator), new Promise((_, reject) => setTimeout(() => reject(new Error('Frame locator timeout')), 5000))])
3793
+ } catch (e) {
3794
+ console.warn('Warning during frame locator creation:', e.message)
3795
+ throw new Error(`Frame ${JSON.stringify(locator)} could not be accessed`)
3160
3796
  }
3161
3797
 
3162
3798
  const contentFrame = this.frame
@@ -3165,8 +3801,14 @@ class Playwright extends Helper {
3165
3801
  this.context = contentFrame
3166
3802
  this.contextLocator = null
3167
3803
  } else {
3168
- this.context = this.page.frame(this.page.frames()[1].name())
3169
- this.contextLocator = locator
3804
+ try {
3805
+ this.context = this.page.frame(this.page.frames()[1].name())
3806
+ this.contextLocator = locator
3807
+ } catch (e) {
3808
+ console.warn('Warning during frame context setup:', e.message)
3809
+ this.context = this.page
3810
+ this.contextLocator = null
3811
+ }
3170
3812
  }
3171
3813
  }
3172
3814
 
@@ -3664,7 +4306,7 @@ class Playwright extends Helper {
3664
4306
  }
3665
4307
  }
3666
4308
 
3667
- module.exports = Playwright
4309
+ export default Playwright
3668
4310
 
3669
4311
  function buildCustomLocatorString(locator) {
3670
4312
  // Note: this.debug not available in standalone function, using console.log
@@ -3682,10 +4324,40 @@ function buildLocatorString(locator) {
3682
4324
  return locator.simplify()
3683
4325
  }
3684
4326
 
4327
+ /**
4328
+ * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4329
+ */
4330
+ function isRoleLocatorObject(locator) {
4331
+ return locator && typeof locator === 'object' && locator.role && !locator.type
4332
+ }
4333
+
4334
+ /**
4335
+ * Handles role locator objects by converting them to Playwright's getByRole() API
4336
+ * Returns elements array if role locator, null otherwise
4337
+ */
4338
+ async function handleRoleLocator(context, locator) {
4339
+ if (!isRoleLocatorObject(locator)) return null
4340
+
4341
+ const options = {}
4342
+ if (locator.text) options.name = locator.text
4343
+ if (locator.exact !== undefined) options.exact = locator.exact
4344
+
4345
+ return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4346
+ }
4347
+
3685
4348
  async function findElements(matcher, locator) {
3686
- if (locator.react) return findReact(matcher, locator)
3687
- if (locator.vue) return findVue(matcher, locator)
3688
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4349
+ // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4350
+ const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4351
+ const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4352
+ const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4353
+
4354
+ if (isReactLocator) return findReact(matcher, locator)
4355
+ if (isVueLocator) return findVue(matcher, locator)
4356
+ if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4357
+
4358
+ // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
4359
+ const roleElements = await handleRoleLocator(matcher, locator)
4360
+ if (roleElements) return roleElements
3689
4361
 
3690
4362
  locator = new Locator(locator, 'css')
3691
4363
 
@@ -3716,8 +4388,16 @@ async function findElements(matcher, locator) {
3716
4388
  }
3717
4389
 
3718
4390
  async function findCustomElements(matcher, locator) {
3719
- const customLocatorStrategies = this.customLocatorStrategies || globalCustomLocatorStrategies
3720
- const strategyFunction = customLocatorStrategies.get ? customLocatorStrategies.get(locator.type) : customLocatorStrategies[locator.type]
4391
+ // Always prioritize this.customLocatorStrategies which is set in constructor from config
4392
+ // and persists in every worker thread instance
4393
+ let strategyFunction = null
4394
+
4395
+ if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
4396
+ strategyFunction = this.customLocatorStrategies[locator.type]
4397
+ } else if (globalCustomLocatorStrategies.has(locator.type)) {
4398
+ // Fallback to global registry (populated in constructor and _init)
4399
+ strategyFunction = globalCustomLocatorStrategies.get(locator.type)
4400
+ }
3721
4401
 
3722
4402
  if (!strategyFunction) {
3723
4403
  throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
@@ -3853,15 +4533,26 @@ async function proceedClick(locator, context = null, options = {}) {
3853
4533
  }
3854
4534
 
3855
4535
  async function findClickable(matcher, locator) {
3856
- if (locator.react) return findReact(matcher, locator)
3857
- if (locator.vue) return findVue(matcher, locator)
3858
- if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4536
+ const matchedLocator = new Locator(locator)
3859
4537
 
3860
- locator = new Locator(locator)
3861
- if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
4538
+ if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
3862
4539
 
3863
4540
  let els
3864
- const literal = xpathLocator.literal(locator.value)
4541
+ const literal = xpathLocator.literal(matchedLocator.value)
4542
+
4543
+ try {
4544
+ els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4545
+ if (els.length) return els
4546
+ } catch (err) {
4547
+ // getByRole not supported or failed
4548
+ }
4549
+
4550
+ try {
4551
+ els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4552
+ if (els.length) return els
4553
+ } catch (err) {
4554
+ // getByRole not supported or failed
4555
+ }
3865
4556
 
3866
4557
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
3867
4558
  if (els.length) return els
@@ -3876,7 +4567,7 @@ async function findClickable(matcher, locator) {
3876
4567
  // Do nothing
3877
4568
  }
3878
4569
 
3879
- return findElements.call(this, matcher, locator.value) // by css or xpath
4570
+ return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
3880
4571
  }
3881
4572
 
3882
4573
  async function proceedSee(assertType, text, context, strict = false) {
@@ -3915,12 +4606,16 @@ async function findCheckable(locator, context) {
3915
4606
  contextEl = contextEl[0]
3916
4607
  }
3917
4608
 
4609
+ // Handle role locators with text/exact options
4610
+ const roleElements = await handleRoleLocator(contextEl, locator)
4611
+ if (roleElements) return roleElements
4612
+
3918
4613
  const matchedLocator = new Locator(locator)
3919
4614
  if (!matchedLocator.isFuzzy()) {
3920
- return findElements.call(this, contextEl, matchedLocator.simplify())
4615
+ return findElements.call(this, contextEl, matchedLocator)
3921
4616
  }
3922
4617
 
3923
- const literal = xpathLocator.literal(locator)
4618
+ const literal = xpathLocator.literal(matchedLocator.value)
3924
4619
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
3925
4620
  if (els.length) {
3926
4621
  return els
@@ -3929,7 +4624,7 @@ async function findCheckable(locator, context) {
3929
4624
  if (els.length) {
3930
4625
  return els
3931
4626
  }
3932
- return findElements.call(this, contextEl, locator)
4627
+ return findElements.call(this, contextEl, matchedLocator.value)
3933
4628
  }
3934
4629
 
3935
4630
  async function proceedIsChecked(assertType, option) {
@@ -3941,6 +4636,13 @@ async function proceedIsChecked(assertType, option) {
3941
4636
  }
3942
4637
 
3943
4638
  async function findFields(locator) {
4639
+ // Handle role locators with text/exact options
4640
+ if (isRoleLocatorObject(locator)) {
4641
+ const page = await this.page
4642
+ const roleElements = await handleRoleLocator(page, locator)
4643
+ if (roleElements) return roleElements
4644
+ }
4645
+
3944
4646
  const matchedLocator = new Locator(locator)
3945
4647
  if (!matchedLocator.isFuzzy()) {
3946
4648
  return this._locate(matchedLocator)
@@ -4212,36 +4914,72 @@ async function clickablePoint(el) {
4212
4914
  }
4213
4915
 
4214
4916
  async function refreshContextSession() {
4215
- // close other sessions
4917
+ // close other sessions with timeout protection, but preserve active session contexts
4216
4918
  try {
4217
- const contexts = await this.browser.contexts()
4218
- contexts.shift()
4919
+ const contexts = await Promise.race([this.browser.contexts(), new Promise((_, reject) => setTimeout(() => reject(new Error('Get contexts timeout')), 3000))])
4219
4920
 
4220
- await Promise.all(contexts.map(c => c.close()))
4921
+ // Keep the first context (default) and any contexts that belong to active sessions
4922
+ const defaultContext = contexts.shift()
4923
+ const activeSessionContexts = new Set()
4924
+
4925
+ // Identify contexts that are still in use by active sessions
4926
+ if (this.sessionPages) {
4927
+ for (const sessionName in this.sessionPages) {
4928
+ const sessionPage = this.sessionPages[sessionName]
4929
+ if (sessionPage && sessionPage.context) {
4930
+ activeSessionContexts.add(sessionPage.context)
4931
+ }
4932
+ }
4933
+ }
4934
+
4935
+ // Only close contexts that are not in use by active sessions
4936
+ const contextsToClose = contexts.filter(context => !activeSessionContexts.has(context))
4937
+
4938
+ if (contextsToClose.length > 0) {
4939
+ await Promise.race([Promise.all(contextsToClose.map(c => c.close())), new Promise((_, reject) => setTimeout(() => reject(new Error('Close contexts timeout')), 5000))])
4940
+ }
4221
4941
  } catch (e) {
4222
- console.log(e)
4942
+ console.warn('Warning during context cleanup:', e.message)
4223
4943
  }
4224
4944
 
4225
4945
  if (this.page) {
4226
- const existingPages = await this.browserContext.pages()
4227
- await this._setPage(existingPages[0])
4946
+ try {
4947
+ const existingPages = await this.browserContext.pages()
4948
+ await this._setPage(existingPages[0])
4949
+ } catch (e) {
4950
+ console.warn('Warning during page setup:', e.message)
4951
+ }
4228
4952
  }
4229
4953
 
4230
4954
  if (this.options.keepBrowserState) return
4231
4955
 
4232
4956
  if (!this.options.keepCookies) {
4233
4957
  this.debugSection('Session', 'cleaning cookies and localStorage')
4234
- await this.clearCookie()
4958
+ try {
4959
+ await this.clearCookie()
4960
+ } catch (e) {
4961
+ console.warn('Warning during cookie cleanup:', e.message)
4962
+ }
4235
4963
  }
4236
- const currentUrl = await this.grabCurrentUrl()
4237
4964
 
4238
- if (currentUrl.startsWith('http')) {
4239
- await this.executeScript('localStorage.clear();').catch(err => {
4240
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4241
- })
4242
- await this.executeScript('sessionStorage.clear();').catch(err => {
4243
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4244
- })
4965
+ try {
4966
+ if (!this.page || !this.browserContext) {
4967
+ this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
4968
+ return
4969
+ }
4970
+
4971
+ const currentUrl = await this.grabCurrentUrl()
4972
+
4973
+ if (currentUrl.startsWith('http')) {
4974
+ await this.executeScript('localStorage.clear();').catch(err => {
4975
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4976
+ })
4977
+ await this.executeScript('sessionStorage.clear();').catch(err => {
4978
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
4979
+ })
4980
+ }
4981
+ } catch (e) {
4982
+ console.warn('Warning during storage cleanup:', e.message)
4245
4983
  }
4246
4984
  }
4247
4985