codeceptjs 4.0.0-beta.5 → 4.0.0-beta.7.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.
- package/README.md +0 -45
- package/bin/codecept.js +46 -57
- package/lib/actor.js +15 -11
- package/lib/ai.js +6 -5
- package/lib/assert/empty.js +9 -8
- package/lib/assert/equal.js +15 -17
- package/lib/assert/error.js +2 -2
- package/lib/assert/include.js +9 -11
- package/lib/assert/throws.js +1 -1
- package/lib/assert/truth.js +8 -5
- package/lib/assert.js +18 -18
- package/lib/codecept.js +66 -107
- package/lib/colorUtils.js +48 -50
- package/lib/command/check.js +32 -27
- package/lib/command/configMigrate.js +11 -10
- package/lib/command/definitions.js +16 -10
- package/lib/command/dryRun.js +16 -16
- package/lib/command/generate.js +29 -26
- package/lib/command/gherkin/init.js +36 -38
- package/lib/command/gherkin/snippets.js +14 -14
- package/lib/command/gherkin/steps.js +21 -18
- package/lib/command/info.js +8 -8
- package/lib/command/init.js +34 -31
- package/lib/command/interactive.js +11 -10
- package/lib/command/list.js +10 -9
- package/lib/command/run-multiple/chunk.js +5 -5
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +3 -3
- package/lib/command/run-multiple.js +16 -13
- package/lib/command/run-rerun.js +6 -7
- package/lib/command/run-workers.js +10 -24
- package/lib/command/run.js +8 -8
- package/lib/command/utils.js +20 -18
- package/lib/command/workers/runTests.js +117 -269
- package/lib/config.js +111 -49
- package/lib/container.js +299 -102
- package/lib/data/context.js +6 -5
- package/lib/data/dataScenarioConfig.js +1 -1
- package/lib/data/dataTableArgument.js +1 -1
- package/lib/data/table.js +1 -1
- package/lib/effects.js +94 -10
- package/lib/els.js +11 -9
- package/lib/event.js +11 -10
- package/lib/globals.js +141 -0
- package/lib/heal.js +12 -12
- package/lib/helper/AI.js +1 -1
- package/lib/helper/ApiDataFactory.js +16 -13
- package/lib/helper/FileSystem.js +32 -12
- package/lib/helper/GraphQL.js +1 -1
- package/lib/helper/GraphQLDataFactory.js +1 -1
- package/lib/helper/JSONResponse.js +19 -30
- package/lib/helper/Mochawesome.js +9 -28
- package/lib/helper/Playwright.js +668 -265
- package/lib/helper/Puppeteer.js +284 -169
- package/lib/helper/REST.js +29 -12
- package/lib/helper/WebDriver.js +192 -71
- package/lib/helper/errors/ConnectionRefused.js +6 -6
- package/lib/helper/errors/ElementAssertion.js +11 -16
- package/lib/helper/errors/ElementNotFound.js +5 -9
- package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
- package/lib/helper/extras/Console.js +11 -11
- package/lib/helper/extras/PlaywrightLocator.js +110 -0
- package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
- package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
- package/lib/helper/extras/Popup.js +1 -1
- package/lib/helper/extras/React.js +29 -30
- package/lib/helper/network/actions.js +33 -48
- package/lib/helper/network/utils.js +76 -83
- package/lib/helper/scripts/blurElement.js +6 -6
- package/lib/helper/scripts/focusElement.js +6 -6
- package/lib/helper/scripts/highlightElement.js +9 -9
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -1
- package/lib/history.js +23 -20
- package/lib/hooks.js +10 -10
- package/lib/html.js +90 -100
- package/lib/index.js +48 -21
- package/lib/listener/config.js +8 -9
- package/lib/listener/emptyRun.js +6 -7
- package/lib/listener/exit.js +4 -3
- package/lib/listener/globalRetry.js +5 -5
- package/lib/listener/globalTimeout.js +11 -10
- package/lib/listener/helpers.js +33 -14
- package/lib/listener/mocha.js +3 -4
- package/lib/listener/result.js +4 -5
- package/lib/listener/steps.js +7 -18
- package/lib/listener/store.js +3 -3
- package/lib/locator.js +213 -192
- package/lib/mocha/asyncWrapper.js +108 -75
- package/lib/mocha/bdd.js +99 -13
- package/lib/mocha/cli.js +60 -27
- package/lib/mocha/factory.js +75 -19
- package/lib/mocha/featureConfig.js +1 -1
- package/lib/mocha/gherkin.js +57 -25
- package/lib/mocha/hooks.js +12 -3
- package/lib/mocha/index.js +13 -4
- package/lib/mocha/inject.js +22 -5
- package/lib/mocha/scenarioConfig.js +2 -2
- package/lib/mocha/suite.js +9 -2
- package/lib/mocha/test.js +10 -13
- package/lib/mocha/ui.js +28 -31
- package/lib/output.js +11 -9
- package/lib/parser.js +44 -44
- package/lib/pause.js +15 -16
- package/lib/plugin/analyze.js +19 -12
- package/lib/plugin/auth.js +20 -21
- package/lib/plugin/autoDelay.js +12 -8
- package/lib/plugin/coverage.js +12 -8
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +3 -2
- package/lib/plugin/heal.js +14 -9
- package/lib/plugin/pageInfo.js +10 -10
- package/lib/plugin/pauseOnFail.js +4 -3
- package/lib/plugin/retryFailedStep.js +47 -5
- package/lib/plugin/screenshotOnFail.js +75 -37
- package/lib/plugin/stepByStepReport.js +14 -14
- package/lib/plugin/stepTimeout.js +4 -3
- package/lib/plugin/subtitles.js +6 -5
- package/lib/recorder.js +33 -23
- package/lib/rerun.js +69 -26
- package/lib/result.js +4 -4
- package/lib/secret.js +18 -17
- package/lib/session.js +95 -89
- package/lib/step/base.js +6 -6
- package/lib/step/config.js +1 -1
- package/lib/step/func.js +3 -3
- package/lib/step/helper.js +3 -3
- package/lib/step/meta.js +4 -4
- package/lib/step/record.js +11 -11
- package/lib/step/retry.js +3 -3
- package/lib/step/section.js +3 -3
- package/lib/step.js +7 -10
- package/lib/steps.js +9 -5
- package/lib/store.js +1 -1
- package/lib/timeout.js +1 -7
- package/lib/transform.js +8 -8
- package/lib/translation.js +32 -18
- package/lib/utils.js +68 -97
- package/lib/workerStorage.js +16 -17
- package/lib/workers.js +145 -171
- package/package.json +58 -55
- package/translations/de-DE.js +2 -2
- package/translations/fr-FR.js +2 -2
- package/translations/index.js +23 -10
- package/translations/it-IT.js +2 -2
- package/translations/ja-JP.js +2 -2
- package/translations/nl-NL.js +2 -2
- package/translations/pl-PL.js +2 -2
- package/translations/pt-BR.js +2 -2
- package/translations/ru-RU.js +2 -2
- package/translations/utils.js +4 -3
- package/translations/zh-CN.js +2 -2
- package/translations/zh-TW.js +2 -2
- package/typings/index.d.ts +7 -18
- package/typings/promiseBasedTypes.d.ts +3769 -5450
- package/typings/types.d.ts +3953 -5778
- package/bin/test-server.js +0 -53
- package/lib/element/WebElement.js +0 -327
- package/lib/helper/Nightmare.js +0 -1486
- package/lib/helper/Protractor.js +0 -1840
- package/lib/helper/TestCafe.js +0 -1391
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -61
- package/lib/listener/retryEnhancer.js +0 -85
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/autoLogin.js +0 -5
- package/lib/plugin/commentStep.js +0 -141
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/htmlReporter.js +0 -1947
- package/lib/plugin/retryTo.js +0 -16
- package/lib/plugin/selenoid.js +0 -364
- package/lib/plugin/standardActingHelpers.js +0 -6
- package/lib/plugin/tryTo.js +0 -16
- package/lib/plugin/wdio.js +0 -247
- package/lib/test-server.js +0 -323
- package/lib/within.js +0 -90
package/lib/helper/Playwright.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
581
|
-
|
|
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 (
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
//
|
|
670
|
+
// Force cleanup of any remaining browser processes
|
|
595
671
|
try {
|
|
596
|
-
if ((
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
610
|
-
|
|
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)
|
|
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
|
-
|
|
636
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
-
|
|
2082
|
-
const
|
|
2083
|
-
|
|
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
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
|
|
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
|
|
2392
|
-
|
|
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
|
-
|
|
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
|
|
2426
|
-
|
|
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
|
-
|
|
2790
|
-
(
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
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
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
this.page.waitForFunction(
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
if (
|
|
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
|
-
|
|
2829
|
-
{ timeout: timeoutGap },
|
|
3227
|
+
{ retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
|
|
2830
3228
|
),
|
|
2831
|
-
|
|
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
|
-
|
|
2889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
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
|
-
|
|
2924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
3946
|
+
return findElements.call(this, contextEl, matchedLocator)
|
|
3566
3947
|
}
|
|
3567
3948
|
|
|
3568
|
-
const literal = xpathLocator.literal(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4268
|
+
console.warn('Warning during context cleanup:', e.message)
|
|
3870
4269
|
}
|
|
3871
4270
|
|
|
3872
4271
|
if (this.page) {
|
|
3873
|
-
|
|
3874
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3886
|
-
await this.
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
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
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
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) {
|