codeceptjs 4.0.0-beta.4 → 4.0.0-beta.6.esm-aria
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -119
- package/bin/codecept.js +53 -54
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +70 -102
- package/lib/ai.js +131 -121
- package/lib/assert/empty.js +11 -12
- package/lib/assert/equal.js +16 -21
- package/lib/assert/error.js +2 -2
- package/lib/assert/include.js +11 -15
- package/lib/assert/throws.js +3 -5
- package/lib/assert/truth.js +10 -7
- package/lib/assert.js +18 -18
- package/lib/codecept.js +112 -101
- package/lib/colorUtils.js +48 -50
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +13 -14
- package/lib/command/definitions.js +24 -36
- package/lib/command/dryRun.js +16 -16
- package/lib/command/generate.js +38 -39
- package/lib/command/gherkin/init.js +36 -38
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +21 -18
- package/lib/command/info.js +49 -15
- package/lib/command/init.js +41 -37
- package/lib/command/interactive.js +22 -13
- package/lib/command/list.js +11 -10
- package/lib/command/run-multiple/chunk.js +50 -47
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +3 -3
- package/lib/command/run-multiple.js +27 -47
- package/lib/command/run-rerun.js +6 -7
- package/lib/command/run-workers.js +15 -66
- package/lib/command/run.js +8 -8
- package/lib/command/utils.js +22 -21
- package/lib/command/workers/runTests.js +131 -241
- package/lib/config.js +111 -49
- package/lib/container.js +589 -244
- package/lib/data/context.js +16 -18
- package/lib/data/dataScenarioConfig.js +9 -9
- package/lib/data/dataTableArgument.js +7 -7
- package/lib/data/table.js +6 -12
- package/lib/effects.js +307 -0
- package/lib/els.js +160 -0
- package/lib/event.js +24 -19
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -81
- package/lib/helper/AI.js +3 -2
- package/lib/helper/ApiDataFactory.js +19 -19
- package/lib/helper/Appium.js +47 -51
- package/lib/helper/FileSystem.js +35 -15
- package/lib/helper/GraphQL.js +1 -1
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +72 -45
- package/lib/helper/Mochawesome.js +14 -11
- package/lib/helper/Playwright.js +832 -434
- package/lib/helper/Puppeteer.js +393 -292
- package/lib/helper/REST.js +32 -27
- package/lib/helper/WebDriver.js +320 -219
- 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 +22 -22
- 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 +54 -0
- package/lib/listener/exit.js +10 -12
- package/lib/listener/{retry.js → globalRetry.js} +10 -10
- package/lib/listener/globalTimeout.js +166 -0
- package/lib/listener/helpers.js +43 -24
- package/lib/listener/mocha.js +4 -5
- package/lib/listener/result.js +11 -0
- package/lib/listener/steps.js +26 -23
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +213 -192
- package/lib/mocha/asyncWrapper.js +264 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +160 -0
- package/lib/{interfaces → mocha}/featureConfig.js +33 -13
- package/lib/{interfaces → mocha}/gherkin.js +75 -45
- package/lib/mocha/hooks.js +121 -0
- package/lib/mocha/index.js +21 -0
- package/lib/mocha/inject.js +46 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +32 -8
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +178 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +229 -0
- package/lib/output.js +86 -64
- package/lib/parser.js +44 -44
- package/lib/pause.js +160 -139
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +137 -43
- package/lib/plugin/autoDelay.js +19 -15
- package/lib/plugin/coverage.js +22 -27
- package/lib/plugin/customLocator.js +5 -5
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/heal.js +49 -17
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +4 -3
- package/lib/plugin/retryFailedStep.js +60 -19
- package/lib/plugin/screenshotOnFail.js +80 -83
- package/lib/plugin/stepByStepReport.js +70 -31
- package/lib/plugin/stepTimeout.js +7 -13
- package/lib/plugin/subtitles.js +10 -9
- package/lib/recorder.js +167 -126
- package/lib/rerun.js +94 -50
- package/lib/result.js +161 -0
- package/lib/secret.js +18 -17
- package/lib/session.js +95 -89
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +18 -332
- package/lib/steps.js +54 -0
- package/lib/store.js +37 -5
- package/lib/template/heal.js +2 -11
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +32 -18
- package/lib/utils.js +354 -250
- package/lib/workerStorage.js +16 -16
- package/lib/workers.js +366 -282
- package/package.json +107 -95
- package/translations/de-DE.js +5 -4
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +23 -9
- package/translations/it-IT.js +5 -4
- package/translations/ja-JP.js +5 -4
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +5 -4
- package/translations/pt-BR.js +5 -4
- package/translations/ru-RU.js +5 -4
- package/translations/utils.js +18 -0
- package/translations/zh-CN.js +5 -4
- package/translations/zh-TW.js +5 -4
- package/typings/index.d.ts +177 -186
- package/typings/promiseBasedTypes.d.ts +3573 -5941
- package/typings/types.d.ts +4042 -6370
- package/lib/cli.js +0 -256
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/Nightmare.js +0 -1504
- package/lib/helper/Protractor.js +0 -1863
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1414
- 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 -62
- package/lib/interfaces/bdd.js +0 -81
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -113
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/commentStep.js +0 -136
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -127
- package/lib/plugin/selenoid.js +0 -384
- package/lib/plugin/standardActingHelpers.js +0 -3
- package/lib/plugin/tryTo.js +0 -115
- package/lib/plugin/wdio.js +0 -249
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
- package/lib/within.js +0 -70
package/lib/helper/Playwright.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
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 {
|
|
16
15
|
xpathLocator,
|
|
17
16
|
ucfirst,
|
|
18
17
|
fileExists,
|
|
@@ -24,13 +23,14 @@ const {
|
|
|
24
23
|
clearString,
|
|
25
24
|
requireWithFallback,
|
|
26
25
|
normalizeSpacesInString,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
relativeDir,
|
|
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'
|
|
34
34
|
|
|
35
35
|
let playwright
|
|
36
36
|
let perfTiming
|
|
@@ -40,26 +40,10 @@ const popupStore = new Popup()
|
|
|
40
40
|
const consoleLogStore = new Console()
|
|
41
41
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
restartsBrowser,
|
|
48
|
-
} = require('./extras/PlaywrightRestartOpts')
|
|
49
|
-
const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine')
|
|
50
|
-
const {
|
|
51
|
-
seeElementError,
|
|
52
|
-
dontSeeElementError,
|
|
53
|
-
dontSeeElementInDOMError,
|
|
54
|
-
seeElementInDOMError,
|
|
55
|
-
} = require('./errors/ElementAssertion')
|
|
56
|
-
const {
|
|
57
|
-
dontSeeTraffic,
|
|
58
|
-
seeTraffic,
|
|
59
|
-
grabRecordedNetworkTraffics,
|
|
60
|
-
stopRecordingTraffic,
|
|
61
|
-
flushNetworkTraffics,
|
|
62
|
-
} = require('./network/actions')
|
|
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'
|
|
63
47
|
|
|
64
48
|
const pathSeparator = path.sep
|
|
65
49
|
|
|
@@ -75,7 +59,6 @@ const pathSeparator = path.sep
|
|
|
75
59
|
* @prop {boolean} [show=true] - show browser window.
|
|
76
60
|
* @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
|
|
77
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.
|
|
78
|
-
* * 'browser' or **true** - closes browser and opens it again between tests.
|
|
79
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
|
|
80
63
|
* @prop {number} [timeout=1000] - - [timeout](https://playwright.dev/docs/api/class-page#page-set-default-timeout) in ms of all Playwright actions .
|
|
81
64
|
* @prop {boolean} [disableScreenshots=false] - don't save screenshot on failure.
|
|
@@ -334,7 +317,7 @@ class Playwright extends Helper {
|
|
|
334
317
|
constructor(config) {
|
|
335
318
|
super(config)
|
|
336
319
|
|
|
337
|
-
playwright
|
|
320
|
+
// playwright will be loaded dynamically in _init method
|
|
338
321
|
|
|
339
322
|
// set defaults
|
|
340
323
|
this.isRemoteBrowser = false
|
|
@@ -358,6 +341,10 @@ class Playwright extends Helper {
|
|
|
358
341
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
359
342
|
this.cdpSession = null
|
|
360
343
|
|
|
344
|
+
// Add test failure tracking to prevent false positives
|
|
345
|
+
this.testFailures = []
|
|
346
|
+
this.hasCleanupError = false
|
|
347
|
+
|
|
361
348
|
// override defaults with config
|
|
362
349
|
this._setConfig(config)
|
|
363
350
|
}
|
|
@@ -392,9 +379,7 @@ class Playwright extends Helper {
|
|
|
392
379
|
config = Object.assign(defaults, config)
|
|
393
380
|
|
|
394
381
|
if (availableBrowsers.indexOf(config.browser) < 0) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`,
|
|
397
|
-
)
|
|
382
|
+
throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`)
|
|
398
383
|
}
|
|
399
384
|
|
|
400
385
|
return config
|
|
@@ -440,9 +425,7 @@ class Playwright extends Helper {
|
|
|
440
425
|
}
|
|
441
426
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
442
427
|
this.isElectron = this.options.browser === 'electron'
|
|
443
|
-
this.userDataDir = this.playwrightOptions.userDataDir
|
|
444
|
-
? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}`
|
|
445
|
-
: undefined
|
|
428
|
+
this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined
|
|
446
429
|
this.isCDPConnection = this.playwrightOptions.cdpConnection
|
|
447
430
|
popupStore.defaultAction = this.options.defaultPopupAction
|
|
448
431
|
}
|
|
@@ -458,27 +441,44 @@ class Playwright extends Helper {
|
|
|
458
441
|
name: 'url',
|
|
459
442
|
message: 'Base url of site to be tested',
|
|
460
443
|
default: 'http://localhost',
|
|
461
|
-
when:
|
|
444
|
+
when: answers => answers.Playwright_browser !== 'electron',
|
|
462
445
|
},
|
|
463
446
|
{
|
|
464
447
|
name: 'show',
|
|
465
448
|
message: 'Show browser window',
|
|
466
449
|
default: true,
|
|
467
450
|
type: 'confirm',
|
|
468
|
-
when:
|
|
451
|
+
when: answers => answers.Playwright_browser !== 'electron',
|
|
469
452
|
},
|
|
470
453
|
]
|
|
471
454
|
}
|
|
472
455
|
|
|
473
456
|
static _checkRequirements() {
|
|
474
457
|
try {
|
|
475
|
-
|
|
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
|
|
476
461
|
} catch (e) {
|
|
477
462
|
return ['playwright@^1.18']
|
|
478
463
|
}
|
|
479
464
|
}
|
|
480
465
|
|
|
481
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
|
+
|
|
482
482
|
// register an internal selector engine for reading value property of elements in a selector
|
|
483
483
|
if (defaultSelectorEnginesInitialized) return
|
|
484
484
|
defaultSelectorEnginesInitialized = true
|
|
@@ -492,7 +492,9 @@ class Playwright extends Helper {
|
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
_beforeSuite() {
|
|
495
|
-
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) {
|
|
496
498
|
this.debugSection('Session', 'Starting singleton browser session')
|
|
497
499
|
return this._startBrowser()
|
|
498
500
|
}
|
|
@@ -500,9 +502,22 @@ class Playwright extends Helper {
|
|
|
500
502
|
|
|
501
503
|
async _before(test) {
|
|
502
504
|
this.currentRunningTest = test
|
|
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
|
+
|
|
503
518
|
recorder.retry({
|
|
504
|
-
retries:
|
|
505
|
-
when:
|
|
519
|
+
retries: test?.opts?.conditionalRetries || 3,
|
|
520
|
+
when: err => {
|
|
506
521
|
if (!err || typeof err.message !== 'string') {
|
|
507
522
|
return false
|
|
508
523
|
}
|
|
@@ -511,7 +526,6 @@ class Playwright extends Helper {
|
|
|
511
526
|
},
|
|
512
527
|
})
|
|
513
528
|
|
|
514
|
-
if (restartsBrowser() && !this.options.manualStart) await this._startBrowser()
|
|
515
529
|
if (!this.isRunning && !this.options.manualStart) await this._startBrowser()
|
|
516
530
|
|
|
517
531
|
this.isAuthenticated = false
|
|
@@ -540,12 +554,25 @@ class Playwright extends Helper {
|
|
|
540
554
|
this.currentRunningTest.artifacts.har = fileName
|
|
541
555
|
contextOptions.recordHar = this.options.recordHar
|
|
542
556
|
}
|
|
557
|
+
|
|
558
|
+
// load pre-saved cookies
|
|
559
|
+
if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
|
|
560
|
+
|
|
543
561
|
if (this.storageState) contextOptions.storageState = this.storageState
|
|
544
562
|
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
|
|
545
563
|
if (this.options.locale) contextOptions.locale = this.options.locale
|
|
546
564
|
if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
|
|
547
565
|
this.contextOptions = contextOptions
|
|
548
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
|
+
}
|
|
575
|
+
this.debugSection('New Session', JSON.stringify(this.contextOptions))
|
|
549
576
|
this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
|
|
550
577
|
}
|
|
551
578
|
}
|
|
@@ -559,10 +586,7 @@ class Playwright extends Helper {
|
|
|
559
586
|
mainPage = existingPages[0] || (await this.browserContext.newPage())
|
|
560
587
|
} catch (e) {
|
|
561
588
|
if (this.playwrightOptions.userDataDir) {
|
|
562
|
-
this.browser = await playwright[this.options.browser].launchPersistentContext(
|
|
563
|
-
this.userDataDir,
|
|
564
|
-
this.playwrightOptions,
|
|
565
|
-
)
|
|
589
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
|
|
566
590
|
this.browserContext = this.browser
|
|
567
591
|
const existingPages = await this.browserContext.pages()
|
|
568
592
|
mainPage = existingPages[0]
|
|
@@ -573,6 +597,15 @@ class Playwright extends Helper {
|
|
|
573
597
|
|
|
574
598
|
await this._setPage(mainPage)
|
|
575
599
|
|
|
600
|
+
try {
|
|
601
|
+
// set metadata for reporting
|
|
602
|
+
test.meta.browser = this.browser.browserType().name()
|
|
603
|
+
test.meta.browserVersion = this.browser.version()
|
|
604
|
+
test.meta.windowSize = `${this.page.viewportSize().width}x${this.page.viewportSize().height}`
|
|
605
|
+
} catch (e) {
|
|
606
|
+
this.debug('Failed to set metadata for reporting')
|
|
607
|
+
}
|
|
608
|
+
|
|
576
609
|
if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true })
|
|
577
610
|
|
|
578
611
|
return this.browser
|
|
@@ -582,8 +615,12 @@ class Playwright extends Helper {
|
|
|
582
615
|
if (!this.isRunning) return
|
|
583
616
|
|
|
584
617
|
if (this.isElectron) {
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
}
|
|
587
624
|
return
|
|
588
625
|
}
|
|
589
626
|
|
|
@@ -591,34 +628,154 @@ class Playwright extends Helper {
|
|
|
591
628
|
return refreshContextSession.bind(this)()
|
|
592
629
|
}
|
|
593
630
|
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Force cleanup of any remaining browser processes
|
|
671
|
+
try {
|
|
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}`)
|
|
597
679
|
}
|
|
598
680
|
|
|
599
|
-
//
|
|
681
|
+
// Clean up session pages explicitly to prevent hanging references
|
|
600
682
|
try {
|
|
601
|
-
if ((
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
+
}
|
|
606
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
|
+
}
|
|
607
707
|
|
|
608
|
-
|
|
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
|
+
})
|
|
609
721
|
}
|
|
610
722
|
} catch (e) {
|
|
611
|
-
|
|
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
|
+
}
|
|
612
729
|
}
|
|
613
730
|
|
|
614
|
-
//
|
|
615
|
-
|
|
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
|
+
}
|
|
616
737
|
}
|
|
617
738
|
|
|
618
|
-
_afterSuite() {}
|
|
619
|
-
|
|
620
739
|
async _finishTest() {
|
|
621
|
-
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
|
+
}
|
|
622
779
|
}
|
|
623
780
|
|
|
624
781
|
_session() {
|
|
@@ -637,16 +794,20 @@ class Playwright extends Helper {
|
|
|
637
794
|
page = await browser.firstWindow()
|
|
638
795
|
} else {
|
|
639
796
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
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))])
|
|
642
803
|
} catch (e) {
|
|
804
|
+
console.warn('Warning during context creation:', e.message)
|
|
643
805
|
if (this.playwrightOptions.userDataDir) {
|
|
644
|
-
browserContext = await playwright[this.options.browser].launchPersistentContext(
|
|
645
|
-
`${this.userDataDir}_${this.activeSessionName}`,
|
|
646
|
-
this.playwrightOptions,
|
|
647
|
-
)
|
|
806
|
+
browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions)
|
|
648
807
|
this.browser = browserContext
|
|
649
808
|
page = await browserContext.pages()[0]
|
|
809
|
+
} else {
|
|
810
|
+
throw e
|
|
650
811
|
}
|
|
651
812
|
}
|
|
652
813
|
}
|
|
@@ -660,7 +821,7 @@ class Playwright extends Helper {
|
|
|
660
821
|
stop: async () => {
|
|
661
822
|
// is closed by _after
|
|
662
823
|
},
|
|
663
|
-
loadVars: async
|
|
824
|
+
loadVars: async context => {
|
|
664
825
|
if (context) {
|
|
665
826
|
this.browserContext = context
|
|
666
827
|
const existingPages = await context.pages()
|
|
@@ -668,7 +829,7 @@ class Playwright extends Helper {
|
|
|
668
829
|
return this._setPage(this.sessionPages[this.activeSessionName])
|
|
669
830
|
}
|
|
670
831
|
},
|
|
671
|
-
restoreVars: async
|
|
832
|
+
restoreVars: async session => {
|
|
672
833
|
this.withinLocator = null
|
|
673
834
|
this.browserContext = defaultContext
|
|
674
835
|
|
|
@@ -677,8 +838,28 @@ class Playwright extends Helper {
|
|
|
677
838
|
} else {
|
|
678
839
|
this.activeSessionName = session
|
|
679
840
|
}
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
863
|
|
|
683
864
|
return this._waitForAction()
|
|
684
865
|
},
|
|
@@ -764,21 +945,43 @@ class Playwright extends Helper {
|
|
|
764
945
|
* @param {object} page page to set
|
|
765
946
|
*/
|
|
766
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
|
+
|
|
767
958
|
page = await page
|
|
768
959
|
this._addPopupListener(page)
|
|
769
960
|
this.page = page
|
|
770
961
|
if (!page) return
|
|
771
|
-
this.browserContext.setDefaultTimeout(0)
|
|
772
|
-
page.setDefaultNavigationTimeout(this.options.getPageTimeout)
|
|
773
|
-
page.setDefaultTimeout(this.options.timeout)
|
|
774
962
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
+
}
|
|
782
985
|
}
|
|
783
986
|
|
|
784
987
|
/**
|
|
@@ -793,7 +996,7 @@ class Playwright extends Helper {
|
|
|
793
996
|
return
|
|
794
997
|
}
|
|
795
998
|
page.removeAllListeners('dialog')
|
|
796
|
-
page.on('dialog', async
|
|
999
|
+
page.on('dialog', async dialog => {
|
|
797
1000
|
popupStore.popup = dialog
|
|
798
1001
|
const action = popupStore.actionType || this.options.defaultPopupAction
|
|
799
1002
|
await this._waitForAction()
|
|
@@ -856,16 +1059,13 @@ class Playwright extends Helper {
|
|
|
856
1059
|
throw err
|
|
857
1060
|
}
|
|
858
1061
|
} else if (this.playwrightOptions.userDataDir) {
|
|
859
|
-
this.browser = await playwright[this.options.browser].launchPersistentContext(
|
|
860
|
-
this.userDataDir,
|
|
861
|
-
this.playwrightOptions,
|
|
862
|
-
)
|
|
1062
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions)
|
|
863
1063
|
} else {
|
|
864
1064
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions)
|
|
865
1065
|
}
|
|
866
1066
|
|
|
867
1067
|
// works only for Chromium
|
|
868
|
-
this.browser.on('targetchanged',
|
|
1068
|
+
this.browser.on('targetchanged', target => {
|
|
869
1069
|
this.debugSection('Url', target.url())
|
|
870
1070
|
})
|
|
871
1071
|
|
|
@@ -879,6 +1079,9 @@ class Playwright extends Helper {
|
|
|
879
1079
|
* @param {object} [contextOptions] See https://playwright.dev/docs/api/class-browser#browser-new-context
|
|
880
1080
|
*/
|
|
881
1081
|
async _createContextPage(contextOptions) {
|
|
1082
|
+
if (!this.browser) {
|
|
1083
|
+
throw new Error('Browser not started. Call _startBrowser() first or disable manualStart option.')
|
|
1084
|
+
}
|
|
882
1085
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
883
1086
|
const page = await this.browserContext.newPage()
|
|
884
1087
|
targetCreatedHandler.call(this, page)
|
|
@@ -895,8 +1098,58 @@ class Playwright extends Helper {
|
|
|
895
1098
|
this.context = null
|
|
896
1099
|
this.frame = null
|
|
897
1100
|
popupStore.clear()
|
|
898
|
-
|
|
899
|
-
|
|
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
|
|
900
1153
|
}
|
|
901
1154
|
|
|
902
1155
|
async _evaluateHandeInContext(...args) {
|
|
@@ -913,8 +1166,21 @@ class Playwright extends Helper {
|
|
|
913
1166
|
|
|
914
1167
|
if (frame) {
|
|
915
1168
|
if (Array.isArray(frame)) {
|
|
1169
|
+
// For nested frames, build the complete frame path
|
|
916
1170
|
await this.switchTo(null)
|
|
917
|
-
|
|
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
|
|
918
1184
|
}
|
|
919
1185
|
await this.switchTo(frame)
|
|
920
1186
|
this.withinLocator = new Locator(frame)
|
|
@@ -931,7 +1197,11 @@ class Playwright extends Helper {
|
|
|
931
1197
|
|
|
932
1198
|
async _withinEnd() {
|
|
933
1199
|
this.withinLocator = null
|
|
934
|
-
|
|
1200
|
+
if (this.page) {
|
|
1201
|
+
this.context = await this.page
|
|
1202
|
+
} else {
|
|
1203
|
+
this.context = null
|
|
1204
|
+
}
|
|
935
1205
|
this.contextLocator = null
|
|
936
1206
|
this.frame = null
|
|
937
1207
|
}
|
|
@@ -940,7 +1210,7 @@ class Playwright extends Helper {
|
|
|
940
1210
|
const navigationStart = timing.navigationStart
|
|
941
1211
|
|
|
942
1212
|
const extractedData = {}
|
|
943
|
-
dataNames.forEach(
|
|
1213
|
+
dataNames.forEach(name => {
|
|
944
1214
|
extractedData[name] = timing[name] - navigationStart
|
|
945
1215
|
})
|
|
946
1216
|
|
|
@@ -954,8 +1224,15 @@ class Playwright extends Helper {
|
|
|
954
1224
|
if (this.isElectron) {
|
|
955
1225
|
throw new Error('Cannot open pages inside an Electron container')
|
|
956
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
|
+
|
|
957
1233
|
if (!/^\w+\:(\/\/|.+)/.test(url)) {
|
|
958
|
-
url = this.options.url + (url.startsWith('/') ? url : `/${url}`)
|
|
1234
|
+
url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
|
|
1235
|
+
this.debug(`Changed URL to base url + relative path: ${url}`)
|
|
959
1236
|
}
|
|
960
1237
|
|
|
961
1238
|
if (this.options.basicAuth && this.isAuthenticated !== true) {
|
|
@@ -965,17 +1242,81 @@ class Playwright extends Helper {
|
|
|
965
1242
|
}
|
|
966
1243
|
}
|
|
967
1244
|
|
|
968
|
-
|
|
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
|
+
}
|
|
969
1316
|
|
|
970
1317
|
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
971
1318
|
|
|
972
|
-
perfTiming = this._extractDataFromPerformanceTiming(
|
|
973
|
-
performanceTiming,
|
|
974
|
-
'responseEnd',
|
|
975
|
-
'domInteractive',
|
|
976
|
-
'domContentLoadedEventEnd',
|
|
977
|
-
'loadEventEnd',
|
|
978
|
-
)
|
|
1319
|
+
perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
|
|
979
1320
|
|
|
980
1321
|
return this._waitForAction()
|
|
981
1322
|
}
|
|
@@ -1127,26 +1468,6 @@ class Playwright extends Helper {
|
|
|
1127
1468
|
await this.page.mouse.up()
|
|
1128
1469
|
}
|
|
1129
1470
|
|
|
1130
|
-
/**
|
|
1131
|
-
* Restart browser with a new context and a new page
|
|
1132
|
-
*
|
|
1133
|
-
* ```js
|
|
1134
|
-
* // Restart browser and use a new timezone
|
|
1135
|
-
* I.restartBrowser({ timezoneId: 'America/Phoenix' });
|
|
1136
|
-
* // Open URL in a new page in changed timezone
|
|
1137
|
-
* I.amOnPage('/');
|
|
1138
|
-
* // Restart browser, allow reading/copying of text from/into clipboard in Chrome
|
|
1139
|
-
* I.restartBrowser({ permissions: ['clipboard-read', 'clipboard-write'] });
|
|
1140
|
-
* ```
|
|
1141
|
-
*
|
|
1142
|
-
* @param {object} [contextOptions] [Options for browser context](https://playwright.dev/docs/api/class-browser#browser-new-context) when starting new browser
|
|
1143
|
-
*/
|
|
1144
|
-
async restartBrowser(contextOptions) {
|
|
1145
|
-
await this._stopBrowser()
|
|
1146
|
-
await this._startBrowser()
|
|
1147
|
-
await this._createContextPage(contextOptions)
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
1471
|
/**
|
|
1151
1472
|
* {{> refreshPage }}
|
|
1152
1473
|
*/
|
|
@@ -1197,10 +1518,7 @@ class Playwright extends Helper {
|
|
|
1197
1518
|
return this.executeScript(() => {
|
|
1198
1519
|
const body = document.body
|
|
1199
1520
|
const html = document.documentElement
|
|
1200
|
-
window.scrollTo(
|
|
1201
|
-
0,
|
|
1202
|
-
Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
|
|
1203
|
-
)
|
|
1521
|
+
window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
|
|
1204
1522
|
})
|
|
1205
1523
|
}
|
|
1206
1524
|
|
|
@@ -1287,7 +1605,22 @@ class Playwright extends Helper {
|
|
|
1287
1605
|
|
|
1288
1606
|
if (this.frame) return findElements(this.frame, locator)
|
|
1289
1607
|
|
|
1290
|
-
|
|
1608
|
+
const els = await findElements(context, locator)
|
|
1609
|
+
|
|
1610
|
+
if (store.debugMode) {
|
|
1611
|
+
const previewElements = els.slice(0, 3)
|
|
1612
|
+
let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))
|
|
1613
|
+
if (els.length > 3) htmls.push('...')
|
|
1614
|
+
if (els.length > 1) {
|
|
1615
|
+
this.debugSection(`Elements (${els.length})`, htmls.join('|').trim())
|
|
1616
|
+
} else if (els.length === 1) {
|
|
1617
|
+
this.debugSection('Element', htmls.join('|').trim())
|
|
1618
|
+
} else {
|
|
1619
|
+
this.debug(`No elements found by ${JSON.stringify(locator).slice(0, 50)}....`)
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return els
|
|
1291
1624
|
}
|
|
1292
1625
|
|
|
1293
1626
|
/**
|
|
@@ -1437,10 +1770,10 @@ class Playwright extends Helper {
|
|
|
1437
1770
|
*/
|
|
1438
1771
|
async closeOtherTabs() {
|
|
1439
1772
|
const pages = await this.browserContext.pages()
|
|
1440
|
-
const otherPages = pages.filter(
|
|
1773
|
+
const otherPages = pages.filter(page => page !== this.page)
|
|
1441
1774
|
if (otherPages.length) {
|
|
1442
1775
|
this.debug(`Closing ${otherPages.length} tabs`)
|
|
1443
|
-
return Promise.all(otherPages.map(
|
|
1776
|
+
return Promise.all(otherPages.map(p => p.close()))
|
|
1444
1777
|
}
|
|
1445
1778
|
return Promise.resolve()
|
|
1446
1779
|
}
|
|
@@ -1483,9 +1816,9 @@ class Playwright extends Helper {
|
|
|
1483
1816
|
*/
|
|
1484
1817
|
async seeElement(locator) {
|
|
1485
1818
|
let els = await this._locate(locator)
|
|
1486
|
-
els = await Promise.all(els.map(
|
|
1819
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1487
1820
|
try {
|
|
1488
|
-
return empty('visible elements').negate(els.filter(
|
|
1821
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1489
1822
|
} catch (e) {
|
|
1490
1823
|
dontSeeElementError(locator)
|
|
1491
1824
|
}
|
|
@@ -1497,9 +1830,9 @@ class Playwright extends Helper {
|
|
|
1497
1830
|
*/
|
|
1498
1831
|
async dontSeeElement(locator) {
|
|
1499
1832
|
let els = await this._locate(locator)
|
|
1500
|
-
els = await Promise.all(els.map(
|
|
1833
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1501
1834
|
try {
|
|
1502
|
-
return empty('visible elements').assert(els.filter(
|
|
1835
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1503
1836
|
} catch (e) {
|
|
1504
1837
|
seeElementError(locator)
|
|
1505
1838
|
}
|
|
@@ -1511,7 +1844,7 @@ class Playwright extends Helper {
|
|
|
1511
1844
|
async seeElementInDOM(locator) {
|
|
1512
1845
|
const els = await this._locate(locator)
|
|
1513
1846
|
try {
|
|
1514
|
-
return empty('elements on page').negate(els.filter(
|
|
1847
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1515
1848
|
} catch (e) {
|
|
1516
1849
|
dontSeeElementInDOMError(locator)
|
|
1517
1850
|
}
|
|
@@ -1523,7 +1856,7 @@ class Playwright extends Helper {
|
|
|
1523
1856
|
async dontSeeElementInDOM(locator) {
|
|
1524
1857
|
const els = await this._locate(locator)
|
|
1525
1858
|
try {
|
|
1526
|
-
return empty('elements on a page').assert(els.filter(
|
|
1859
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1527
1860
|
} catch (e) {
|
|
1528
1861
|
seeElementInDOMError(locator)
|
|
1529
1862
|
}
|
|
@@ -1547,7 +1880,7 @@ class Playwright extends Helper {
|
|
|
1547
1880
|
* @return {Promise<void>}
|
|
1548
1881
|
*/
|
|
1549
1882
|
async handleDownloads(fileName) {
|
|
1550
|
-
this.page.waitForEvent('download').then(async
|
|
1883
|
+
this.page.waitForEvent('download').then(async download => {
|
|
1551
1884
|
const filePath = await download.path()
|
|
1552
1885
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
1553
1886
|
|
|
@@ -1741,6 +2074,7 @@ class Playwright extends Helper {
|
|
|
1741
2074
|
const el = els[0]
|
|
1742
2075
|
|
|
1743
2076
|
await el.clear()
|
|
2077
|
+
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
1744
2078
|
|
|
1745
2079
|
await highlightActiveElement.call(this, el)
|
|
1746
2080
|
|
|
@@ -1852,8 +2186,8 @@ class Playwright extends Helper {
|
|
|
1852
2186
|
*/
|
|
1853
2187
|
async grabNumberOfVisibleElements(locator) {
|
|
1854
2188
|
let els = await this._locate(locator)
|
|
1855
|
-
els = await Promise.all(els.map(
|
|
1856
|
-
return els.filter(
|
|
2189
|
+
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2190
|
+
return els.filter(v => v).length
|
|
1857
2191
|
}
|
|
1858
2192
|
|
|
1859
2193
|
/**
|
|
@@ -1963,9 +2297,7 @@ class Playwright extends Helper {
|
|
|
1963
2297
|
*/
|
|
1964
2298
|
async seeNumberOfElements(locator, num) {
|
|
1965
2299
|
const elements = await this._locate(locator)
|
|
1966
|
-
return equals(
|
|
1967
|
-
`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`,
|
|
1968
|
-
).assert(elements.length, num)
|
|
2300
|
+
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
|
|
1969
2301
|
}
|
|
1970
2302
|
|
|
1971
2303
|
/**
|
|
@@ -1975,10 +2307,7 @@ class Playwright extends Helper {
|
|
|
1975
2307
|
*/
|
|
1976
2308
|
async seeNumberOfVisibleElements(locator, num) {
|
|
1977
2309
|
const res = await this.grabNumberOfVisibleElements(locator)
|
|
1978
|
-
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(
|
|
1979
|
-
res,
|
|
1980
|
-
num,
|
|
1981
|
-
)
|
|
2310
|
+
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
|
|
1982
2311
|
}
|
|
1983
2312
|
|
|
1984
2313
|
/**
|
|
@@ -1997,7 +2326,7 @@ class Playwright extends Helper {
|
|
|
1997
2326
|
*/
|
|
1998
2327
|
async seeCookie(name) {
|
|
1999
2328
|
const cookies = await this.browserContext.cookies()
|
|
2000
|
-
empty(`cookie ${name} to be set`).negate(cookies.filter(
|
|
2329
|
+
empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
|
|
2001
2330
|
}
|
|
2002
2331
|
|
|
2003
2332
|
/**
|
|
@@ -2005,7 +2334,7 @@ class Playwright extends Helper {
|
|
|
2005
2334
|
*/
|
|
2006
2335
|
async dontSeeCookie(name) {
|
|
2007
2336
|
const cookies = await this.browserContext.cookies()
|
|
2008
|
-
empty(`cookie ${name} not to be set`).assert(cookies.filter(
|
|
2337
|
+
empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
|
|
2009
2338
|
}
|
|
2010
2339
|
|
|
2011
2340
|
/**
|
|
@@ -2014,19 +2343,31 @@ class Playwright extends Helper {
|
|
|
2014
2343
|
* {{> grabCookie }}
|
|
2015
2344
|
*/
|
|
2016
2345
|
async grabCookie(name) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
+
}
|
|
2021
2361
|
}
|
|
2022
2362
|
|
|
2023
2363
|
/**
|
|
2024
2364
|
* {{> clearCookie }}
|
|
2025
2365
|
*/
|
|
2026
|
-
async clearCookie() {
|
|
2027
|
-
// Playwright currently doesn't support to delete a certain cookie
|
|
2028
|
-
// https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies
|
|
2366
|
+
async clearCookie(cookieName) {
|
|
2029
2367
|
if (!this.browserContext) return
|
|
2368
|
+
if (cookieName) {
|
|
2369
|
+
return this.browserContext.clearCookies({ name: cookieName })
|
|
2370
|
+
}
|
|
2030
2371
|
return this.browserContext.clearCookies()
|
|
2031
2372
|
}
|
|
2032
2373
|
|
|
@@ -2083,9 +2424,28 @@ class Playwright extends Helper {
|
|
|
2083
2424
|
*
|
|
2084
2425
|
*/
|
|
2085
2426
|
async grabTextFrom(locator) {
|
|
2086
|
-
|
|
2087
|
-
const
|
|
2088
|
-
|
|
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)
|
|
2089
2449
|
this.debugSection('Text', text)
|
|
2090
2450
|
return text
|
|
2091
2451
|
}
|
|
@@ -2098,7 +2458,7 @@ class Playwright extends Helper {
|
|
|
2098
2458
|
const els = await this._locate(locator)
|
|
2099
2459
|
const texts = []
|
|
2100
2460
|
for (const el of els) {
|
|
2101
|
-
texts.push(await
|
|
2461
|
+
texts.push(await el.innerText())
|
|
2102
2462
|
}
|
|
2103
2463
|
this.debug(`Matched ${els.length} elements`)
|
|
2104
2464
|
return texts
|
|
@@ -2120,7 +2480,7 @@ class Playwright extends Helper {
|
|
|
2120
2480
|
async grabValueFromAll(locator) {
|
|
2121
2481
|
const els = await findFields.call(this, locator)
|
|
2122
2482
|
this.debug(`Matched ${els.length} elements`)
|
|
2123
|
-
return Promise.all(els.map(
|
|
2483
|
+
return Promise.all(els.map(el => el.inputValue()))
|
|
2124
2484
|
}
|
|
2125
2485
|
|
|
2126
2486
|
/**
|
|
@@ -2139,7 +2499,7 @@ class Playwright extends Helper {
|
|
|
2139
2499
|
async grabHTMLFromAll(locator) {
|
|
2140
2500
|
const els = await this._locate(locator)
|
|
2141
2501
|
this.debug(`Matched ${els.length} elements`)
|
|
2142
|
-
return Promise.all(els.map(
|
|
2502
|
+
return Promise.all(els.map(el => el.innerHTML()))
|
|
2143
2503
|
}
|
|
2144
2504
|
|
|
2145
2505
|
/**
|
|
@@ -2160,11 +2520,7 @@ class Playwright extends Helper {
|
|
|
2160
2520
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2161
2521
|
const els = await this._locate(locator)
|
|
2162
2522
|
this.debug(`Matched ${els.length} elements`)
|
|
2163
|
-
const cssValues = await Promise.all(
|
|
2164
|
-
els.map((el) =>
|
|
2165
|
-
el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty),
|
|
2166
|
-
),
|
|
2167
|
-
)
|
|
2523
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
|
|
2168
2524
|
|
|
2169
2525
|
return cssValues
|
|
2170
2526
|
}
|
|
@@ -2192,19 +2548,16 @@ class Playwright extends Helper {
|
|
|
2192
2548
|
}
|
|
2193
2549
|
}
|
|
2194
2550
|
|
|
2195
|
-
const values = Object.keys(cssPropertiesCamelCase).map(
|
|
2551
|
+
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
|
|
2196
2552
|
if (!Array.isArray(props)) props = [props]
|
|
2197
2553
|
let chunked = chunkArray(props, values.length)
|
|
2198
|
-
chunked = chunked.filter(
|
|
2554
|
+
chunked = chunked.filter(val => {
|
|
2199
2555
|
for (let i = 0; i < val.length; ++i) {
|
|
2200
|
-
// eslint-disable-next-line eqeqeq
|
|
2201
2556
|
if (val[i] != values[i]) return false
|
|
2202
2557
|
}
|
|
2203
2558
|
return true
|
|
2204
2559
|
})
|
|
2205
|
-
return equals(
|
|
2206
|
-
`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
|
|
2207
|
-
).assert(chunked.length, elemAmount)
|
|
2560
|
+
return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
|
|
2208
2561
|
}
|
|
2209
2562
|
|
|
2210
2563
|
/**
|
|
@@ -2217,16 +2570,16 @@ class Playwright extends Helper {
|
|
|
2217
2570
|
|
|
2218
2571
|
const elemAmount = res.length
|
|
2219
2572
|
const commands = []
|
|
2220
|
-
res.forEach(
|
|
2221
|
-
Object.keys(attributes).forEach(
|
|
2573
|
+
res.forEach(el => {
|
|
2574
|
+
Object.keys(attributes).forEach(prop => {
|
|
2222
2575
|
commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop))
|
|
2223
2576
|
})
|
|
2224
2577
|
})
|
|
2225
2578
|
let attrs = await Promise.all(commands)
|
|
2226
|
-
const values = Object.keys(attributes).map(
|
|
2579
|
+
const values = Object.keys(attributes).map(key => attributes[key])
|
|
2227
2580
|
if (!Array.isArray(attrs)) attrs = [attrs]
|
|
2228
2581
|
let chunked = chunkArray(attrs, values.length)
|
|
2229
|
-
chunked = chunked.filter(
|
|
2582
|
+
chunked = chunked.filter(val => {
|
|
2230
2583
|
for (let i = 0; i < val.length; ++i) {
|
|
2231
2584
|
// the attribute could be a boolean
|
|
2232
2585
|
if (typeof val[i] === 'boolean') return val[i] === values[i]
|
|
@@ -2235,10 +2588,7 @@ class Playwright extends Helper {
|
|
|
2235
2588
|
}
|
|
2236
2589
|
return true
|
|
2237
2590
|
})
|
|
2238
|
-
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
|
|
2239
|
-
chunked.length,
|
|
2240
|
-
elemAmount,
|
|
2241
|
-
)
|
|
2591
|
+
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount)
|
|
2242
2592
|
}
|
|
2243
2593
|
|
|
2244
2594
|
/**
|
|
@@ -2297,11 +2647,16 @@ class Playwright extends Helper {
|
|
|
2297
2647
|
async saveElementScreenshot(locator, fileName) {
|
|
2298
2648
|
const outputFile = screenshotOutputFolder(fileName)
|
|
2299
2649
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
+
}
|
|
2305
2660
|
}
|
|
2306
2661
|
|
|
2307
2662
|
/**
|
|
@@ -2311,27 +2666,72 @@ class Playwright extends Helper {
|
|
|
2311
2666
|
const fullPageOption = fullPage || this.options.fullPageScreenshots
|
|
2312
2667
|
let outputFile = screenshotOutputFolder(fileName)
|
|
2313
2668
|
|
|
2314
|
-
this.
|
|
2669
|
+
this.debugSection('Screenshot', relativeDir(outputFile))
|
|
2315
2670
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
+
}
|
|
2679
|
+
|
|
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
|
+
}
|
|
2321
2701
|
|
|
2322
|
-
|
|
2702
|
+
// Handle session screenshots for ALL sessions, not just active one
|
|
2703
|
+
if (this.sessionPages && Object.keys(this.sessionPages).length > 0) {
|
|
2323
2704
|
for (const sessionName in this.sessionPages) {
|
|
2324
|
-
const
|
|
2705
|
+
const sessionPage = this.sessionPages[sessionName]
|
|
2325
2706
|
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
|
|
2326
2707
|
|
|
2327
|
-
this.
|
|
2708
|
+
this.debugSection('Screenshot', `${sessionName} - ${relativeDir(outputFile)}`)
|
|
2328
2709
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
|
2335
2735
|
}
|
|
2336
2736
|
}
|
|
2337
2737
|
}
|
|
@@ -2358,9 +2758,7 @@ class Playwright extends Helper {
|
|
|
2358
2758
|
method = method.toLowerCase()
|
|
2359
2759
|
const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']
|
|
2360
2760
|
if (!allowedMethods.includes(method)) {
|
|
2361
|
-
throw new Error(
|
|
2362
|
-
`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`,
|
|
2363
|
-
)
|
|
2761
|
+
throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`)
|
|
2364
2762
|
}
|
|
2365
2763
|
|
|
2366
2764
|
if (url.startsWith('/')) {
|
|
@@ -2397,10 +2795,7 @@ class Playwright extends Helper {
|
|
|
2397
2795
|
if (this.options.recordVideo && this.page && this.page.video()) {
|
|
2398
2796
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
|
|
2399
2797
|
for (const sessionName in this.sessionPages) {
|
|
2400
|
-
test.artifacts[`video_${sessionName}`] = saveVideoForPage(
|
|
2401
|
-
this.sessionPages[sessionName],
|
|
2402
|
-
`${test.title}_${sessionName}.failed`,
|
|
2403
|
-
)
|
|
2798
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
|
|
2404
2799
|
}
|
|
2405
2800
|
}
|
|
2406
2801
|
|
|
@@ -2408,10 +2803,7 @@ class Playwright extends Helper {
|
|
|
2408
2803
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
|
|
2409
2804
|
for (const sessionName in this.sessionPages) {
|
|
2410
2805
|
if (!this.sessionPages[sessionName].context) continue
|
|
2411
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
|
|
2412
|
-
this.sessionPages[sessionName].context,
|
|
2413
|
-
`${test.title}_${sessionName}.failed`,
|
|
2414
|
-
)
|
|
2806
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
|
|
2415
2807
|
}
|
|
2416
2808
|
}
|
|
2417
2809
|
|
|
@@ -2425,16 +2817,13 @@ class Playwright extends Helper {
|
|
|
2425
2817
|
if (this.options.keepVideoForPassedTests) {
|
|
2426
2818
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
|
|
2427
2819
|
for (const sessionName of Object.keys(this.sessionPages)) {
|
|
2428
|
-
test.artifacts[`video_${sessionName}`] = saveVideoForPage(
|
|
2429
|
-
this.sessionPages[sessionName],
|
|
2430
|
-
`${test.title}_${sessionName}.passed`,
|
|
2431
|
-
)
|
|
2820
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
|
|
2432
2821
|
}
|
|
2433
2822
|
} else {
|
|
2434
2823
|
this.page
|
|
2435
2824
|
.video()
|
|
2436
2825
|
.delete()
|
|
2437
|
-
.catch(
|
|
2826
|
+
.catch(e => {})
|
|
2438
2827
|
}
|
|
2439
2828
|
}
|
|
2440
2829
|
|
|
@@ -2444,10 +2833,7 @@ class Playwright extends Helper {
|
|
|
2444
2833
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
|
|
2445
2834
|
for (const sessionName in this.sessionPages) {
|
|
2446
2835
|
if (!this.sessionPages[sessionName].context) continue
|
|
2447
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(
|
|
2448
|
-
this.sessionPages[sessionName].context,
|
|
2449
|
-
`${test.title}_${sessionName}.passed`,
|
|
2450
|
-
)
|
|
2836
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
|
|
2451
2837
|
}
|
|
2452
2838
|
}
|
|
2453
2839
|
} else {
|
|
@@ -2464,7 +2850,7 @@ class Playwright extends Helper {
|
|
|
2464
2850
|
* {{> wait }}
|
|
2465
2851
|
*/
|
|
2466
2852
|
async wait(sec) {
|
|
2467
|
-
return new Promise(
|
|
2853
|
+
return new Promise(done => {
|
|
2468
2854
|
setTimeout(done, sec * 1000)
|
|
2469
2855
|
})
|
|
2470
2856
|
}
|
|
@@ -2480,20 +2866,18 @@ class Playwright extends Helper {
|
|
|
2480
2866
|
const context = await this._getContext()
|
|
2481
2867
|
if (!locator.isXPath()) {
|
|
2482
2868
|
const valueFn = function ([locator]) {
|
|
2483
|
-
return Array.from(document.querySelectorAll(locator)).filter(
|
|
2869
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0
|
|
2484
2870
|
}
|
|
2485
2871
|
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
|
|
2486
2872
|
} else {
|
|
2487
2873
|
const enabledFn = function ([locator, $XPath]) {
|
|
2488
|
-
eval($XPath)
|
|
2489
|
-
return $XPath(null, locator).filter(
|
|
2874
|
+
eval($XPath)
|
|
2875
|
+
return $XPath(null, locator).filter(el => !el.disabled).length > 0
|
|
2490
2876
|
}
|
|
2491
2877
|
waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
2492
2878
|
}
|
|
2493
|
-
return waiter.catch(
|
|
2494
|
-
throw new Error(
|
|
2495
|
-
`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2496
|
-
)
|
|
2879
|
+
return waiter.catch(err => {
|
|
2880
|
+
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2497
2881
|
})
|
|
2498
2882
|
}
|
|
2499
2883
|
|
|
@@ -2508,20 +2892,18 @@ class Playwright extends Helper {
|
|
|
2508
2892
|
const context = await this._getContext()
|
|
2509
2893
|
if (!locator.isXPath()) {
|
|
2510
2894
|
const valueFn = function ([locator]) {
|
|
2511
|
-
return Array.from(document.querySelectorAll(locator)).filter(
|
|
2895
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0
|
|
2512
2896
|
}
|
|
2513
2897
|
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
|
|
2514
2898
|
} else {
|
|
2515
2899
|
const disabledFn = function ([locator, $XPath]) {
|
|
2516
|
-
eval($XPath)
|
|
2517
|
-
return $XPath(null, locator).filter(
|
|
2900
|
+
eval($XPath)
|
|
2901
|
+
return $XPath(null, locator).filter(el => el.disabled).length > 0
|
|
2518
2902
|
}
|
|
2519
2903
|
waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
2520
2904
|
}
|
|
2521
|
-
return waiter.catch(
|
|
2522
|
-
throw new Error(
|
|
2523
|
-
`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2524
|
-
)
|
|
2905
|
+
return waiter.catch(err => {
|
|
2906
|
+
throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2525
2907
|
})
|
|
2526
2908
|
}
|
|
2527
2909
|
|
|
@@ -2536,26 +2918,21 @@ class Playwright extends Helper {
|
|
|
2536
2918
|
const context = await this._getContext()
|
|
2537
2919
|
if (!locator.isXPath()) {
|
|
2538
2920
|
const valueFn = function ([locator, value]) {
|
|
2539
|
-
return (
|
|
2540
|
-
Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length >
|
|
2541
|
-
0
|
|
2542
|
-
)
|
|
2921
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2543
2922
|
}
|
|
2544
2923
|
waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
|
|
2545
2924
|
} else {
|
|
2546
2925
|
const valueFn = function ([locator, $XPath, value]) {
|
|
2547
|
-
eval($XPath)
|
|
2548
|
-
return $XPath(null, locator).filter(
|
|
2926
|
+
eval($XPath)
|
|
2927
|
+
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2549
2928
|
}
|
|
2550
2929
|
waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
|
|
2551
2930
|
timeout: waitTimeout,
|
|
2552
2931
|
})
|
|
2553
2932
|
}
|
|
2554
|
-
return waiter.catch(
|
|
2933
|
+
return waiter.catch(err => {
|
|
2555
2934
|
const loc = locator.toString()
|
|
2556
|
-
throw new Error(
|
|
2557
|
-
`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2558
|
-
)
|
|
2935
|
+
throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2559
2936
|
})
|
|
2560
2937
|
}
|
|
2561
2938
|
|
|
@@ -2575,22 +2952,20 @@ class Playwright extends Helper {
|
|
|
2575
2952
|
if (!els || els.length === 0) {
|
|
2576
2953
|
return false
|
|
2577
2954
|
}
|
|
2578
|
-
return Array.prototype.filter.call(els,
|
|
2955
|
+
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
|
|
2579
2956
|
}
|
|
2580
2957
|
waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
|
|
2581
2958
|
} else {
|
|
2582
2959
|
const visibleFn = function ([locator, $XPath, num]) {
|
|
2583
|
-
eval($XPath)
|
|
2584
|
-
return $XPath(null, locator).filter(
|
|
2960
|
+
eval($XPath)
|
|
2961
|
+
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
|
|
2585
2962
|
}
|
|
2586
2963
|
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
|
|
2587
2964
|
timeout: waitTimeout,
|
|
2588
2965
|
})
|
|
2589
2966
|
}
|
|
2590
|
-
return waiter.catch(
|
|
2591
|
-
throw new Error(
|
|
2592
|
-
`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2593
|
-
)
|
|
2967
|
+
return waiter.catch(err => {
|
|
2968
|
+
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2594
2969
|
})
|
|
2595
2970
|
}
|
|
2596
2971
|
|
|
@@ -2598,9 +2973,7 @@ class Playwright extends Helper {
|
|
|
2598
2973
|
* {{> waitForClickable }}
|
|
2599
2974
|
*/
|
|
2600
2975
|
async waitForClickable(locator, waitTimeout) {
|
|
2601
|
-
console.log(
|
|
2602
|
-
'I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable',
|
|
2603
|
-
)
|
|
2976
|
+
console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable')
|
|
2604
2977
|
console.log('Remove usage of this function')
|
|
2605
2978
|
}
|
|
2606
2979
|
|
|
@@ -2616,9 +2989,7 @@ class Playwright extends Helper {
|
|
|
2616
2989
|
try {
|
|
2617
2990
|
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
2618
2991
|
} catch (e) {
|
|
2619
|
-
throw new Error(
|
|
2620
|
-
`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`,
|
|
2621
|
-
)
|
|
2992
|
+
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
2622
2993
|
}
|
|
2623
2994
|
}
|
|
2624
2995
|
|
|
@@ -2710,10 +3081,8 @@ class Playwright extends Helper {
|
|
|
2710
3081
|
.locator(buildLocatorString(locator))
|
|
2711
3082
|
.first()
|
|
2712
3083
|
.waitFor({ timeout: waitTimeout, state: 'hidden' })
|
|
2713
|
-
.catch(
|
|
2714
|
-
throw new Error(
|
|
2715
|
-
`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2716
|
-
)
|
|
3084
|
+
.catch(err => {
|
|
3085
|
+
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2717
3086
|
})
|
|
2718
3087
|
}
|
|
2719
3088
|
|
|
@@ -2739,6 +3108,9 @@ class Playwright extends Helper {
|
|
|
2739
3108
|
if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
|
|
2740
3109
|
return this.context
|
|
2741
3110
|
}
|
|
3111
|
+
if (this.frame) {
|
|
3112
|
+
return this.frame
|
|
3113
|
+
}
|
|
2742
3114
|
return this.page
|
|
2743
3115
|
}
|
|
2744
3116
|
|
|
@@ -2750,14 +3122,14 @@ class Playwright extends Helper {
|
|
|
2750
3122
|
|
|
2751
3123
|
return this.page
|
|
2752
3124
|
.waitForFunction(
|
|
2753
|
-
|
|
3125
|
+
urlPart => {
|
|
2754
3126
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2755
3127
|
return currUrl.indexOf(urlPart) > -1
|
|
2756
3128
|
},
|
|
2757
3129
|
urlPart,
|
|
2758
3130
|
{ timeout: waitTimeout },
|
|
2759
3131
|
)
|
|
2760
|
-
.catch(async
|
|
3132
|
+
.catch(async e => {
|
|
2761
3133
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2762
3134
|
if (/Timeout/i.test(e.message)) {
|
|
2763
3135
|
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
@@ -2780,14 +3152,14 @@ class Playwright extends Helper {
|
|
|
2780
3152
|
|
|
2781
3153
|
return this.page
|
|
2782
3154
|
.waitForFunction(
|
|
2783
|
-
|
|
3155
|
+
urlPart => {
|
|
2784
3156
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2785
3157
|
return currUrl.indexOf(urlPart) > -1
|
|
2786
3158
|
},
|
|
2787
3159
|
urlPart,
|
|
2788
3160
|
{ timeout: waitTimeout },
|
|
2789
3161
|
)
|
|
2790
|
-
.catch(async
|
|
3162
|
+
.catch(async e => {
|
|
2791
3163
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2792
3164
|
if (/Timeout/i.test(e.message)) {
|
|
2793
3165
|
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
|
|
@@ -2803,28 +3175,23 @@ class Playwright extends Helper {
|
|
|
2803
3175
|
async waitForText(text, sec = null, context = null) {
|
|
2804
3176
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2805
3177
|
const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`
|
|
2806
|
-
let waiter
|
|
2807
3178
|
|
|
2808
3179
|
const contextObject = await this._getContext()
|
|
2809
3180
|
|
|
2810
3181
|
if (context) {
|
|
2811
3182
|
const locator = new Locator(context, 'css')
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3183
|
+
try {
|
|
3184
|
+
if (!locator.isXPath()) {
|
|
3185
|
+
return contextObject
|
|
2815
3186
|
.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
|
|
2816
3187
|
.first()
|
|
2817
3188
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2818
|
-
} catch (e) {
|
|
2819
|
-
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2820
3189
|
}
|
|
2821
|
-
}
|
|
2822
3190
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
await contextObject.waitForFunction(
|
|
3191
|
+
if (locator.isXPath()) {
|
|
3192
|
+
return contextObject.waitForFunction(
|
|
2826
3193
|
([locator, text, $XPath]) => {
|
|
2827
|
-
eval($XPath)
|
|
3194
|
+
eval($XPath)
|
|
2828
3195
|
const el = $XPath(null, locator)
|
|
2829
3196
|
if (!el.length) return false
|
|
2830
3197
|
return el[0].innerText.indexOf(text) > -1
|
|
@@ -2832,27 +3199,34 @@ class Playwright extends Helper {
|
|
|
2832
3199
|
[locator.value, text, $XPath.toString()],
|
|
2833
3200
|
{ timeout: waitTimeout },
|
|
2834
3201
|
)
|
|
2835
|
-
} catch (e) {
|
|
2836
|
-
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2837
3202
|
}
|
|
3203
|
+
} catch (e) {
|
|
3204
|
+
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2838
3205
|
}
|
|
2839
|
-
} else {
|
|
2840
|
-
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2841
|
-
|
|
2842
|
-
const _contextObject = this.frame ? this.frame : contextObject
|
|
2843
|
-
let count = 0
|
|
2844
|
-
do {
|
|
2845
|
-
waiter = await _contextObject
|
|
2846
|
-
.locator(`:has-text(${JSON.stringify(text)})`)
|
|
2847
|
-
.first()
|
|
2848
|
-
.isVisible()
|
|
2849
|
-
if (waiter) break
|
|
2850
|
-
await this.wait(1)
|
|
2851
|
-
count += 1000
|
|
2852
|
-
} while (count <= waitTimeout)
|
|
2853
|
-
|
|
2854
|
-
if (!waiter) throw new Error(`${errorMessage}`)
|
|
2855
3206
|
}
|
|
3207
|
+
|
|
3208
|
+
const timeoutGap = waitTimeout + 1000
|
|
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
|
|
3214
|
+
return Promise.race([
|
|
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)
|
|
3226
|
+
},
|
|
3227
|
+
{ retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
|
|
3228
|
+
),
|
|
3229
|
+
])
|
|
2856
3230
|
}
|
|
2857
3231
|
|
|
2858
3232
|
/**
|
|
@@ -2902,8 +3276,13 @@ class Playwright extends Helper {
|
|
|
2902
3276
|
}
|
|
2903
3277
|
|
|
2904
3278
|
if (locator >= 0 && locator < childFrames.length) {
|
|
2905
|
-
|
|
2906
|
-
|
|
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
|
+
}
|
|
2907
3286
|
} else {
|
|
2908
3287
|
throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath')
|
|
2909
3288
|
}
|
|
@@ -2919,16 +3298,25 @@ class Playwright extends Helper {
|
|
|
2919
3298
|
|
|
2920
3299
|
// iframe by selector
|
|
2921
3300
|
locator = buildLocatorString(new Locator(locator, 'css'))
|
|
2922
|
-
|
|
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
|
+
}
|
|
2923
3309
|
|
|
2924
3310
|
if (!frame) {
|
|
2925
3311
|
throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`)
|
|
2926
3312
|
}
|
|
2927
3313
|
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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`)
|
|
2932
3320
|
}
|
|
2933
3321
|
|
|
2934
3322
|
const contentFrame = this.frame
|
|
@@ -2937,8 +3325,14 @@ class Playwright extends Helper {
|
|
|
2937
3325
|
this.context = contentFrame
|
|
2938
3326
|
this.contextLocator = null
|
|
2939
3327
|
} else {
|
|
2940
|
-
|
|
2941
|
-
|
|
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
|
+
}
|
|
2942
3336
|
}
|
|
2943
3337
|
}
|
|
2944
3338
|
|
|
@@ -3021,11 +3415,11 @@ class Playwright extends Helper {
|
|
|
3021
3415
|
}
|
|
3022
3416
|
} else {
|
|
3023
3417
|
const visibleFn = function ([locator, $XPath]) {
|
|
3024
|
-
eval($XPath)
|
|
3418
|
+
eval($XPath)
|
|
3025
3419
|
return $XPath(null, locator).length === 0
|
|
3026
3420
|
}
|
|
3027
3421
|
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
|
|
3028
|
-
return waiter.catch(
|
|
3422
|
+
return waiter.catch(err => {
|
|
3029
3423
|
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
3030
3424
|
})
|
|
3031
3425
|
}
|
|
@@ -3047,9 +3441,9 @@ class Playwright extends Helper {
|
|
|
3047
3441
|
|
|
3048
3442
|
return promiseRetry(
|
|
3049
3443
|
async (retry, number) => {
|
|
3050
|
-
const _grabCookie = async
|
|
3444
|
+
const _grabCookie = async name => {
|
|
3051
3445
|
const cookies = await this.browserContext.cookies()
|
|
3052
|
-
const cookie = cookies.filter(
|
|
3446
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
3053
3447
|
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
|
|
3054
3448
|
}
|
|
3055
3449
|
|
|
@@ -3128,7 +3522,7 @@ class Playwright extends Helper {
|
|
|
3128
3522
|
this.recording = true
|
|
3129
3523
|
this.recordedAtLeastOnce = true
|
|
3130
3524
|
|
|
3131
|
-
this.page.on('requestfinished', async
|
|
3525
|
+
this.page.on('requestfinished', async request => {
|
|
3132
3526
|
const information = {
|
|
3133
3527
|
url: request.url(),
|
|
3134
3528
|
method: request.method(),
|
|
@@ -3167,20 +3561,20 @@ class Playwright extends Helper {
|
|
|
3167
3561
|
*/
|
|
3168
3562
|
blockTraffic(urls) {
|
|
3169
3563
|
if (Array.isArray(urls)) {
|
|
3170
|
-
urls.forEach(
|
|
3171
|
-
this.page.route(url,
|
|
3564
|
+
urls.forEach(url => {
|
|
3565
|
+
this.page.route(url, route => {
|
|
3172
3566
|
route
|
|
3173
3567
|
.abort()
|
|
3174
3568
|
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3175
|
-
.catch(
|
|
3569
|
+
.catch(e => {})
|
|
3176
3570
|
})
|
|
3177
3571
|
})
|
|
3178
3572
|
} else {
|
|
3179
|
-
this.page.route(urls,
|
|
3573
|
+
this.page.route(urls, route => {
|
|
3180
3574
|
route
|
|
3181
3575
|
.abort()
|
|
3182
3576
|
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3183
|
-
.catch(
|
|
3577
|
+
.catch(e => {})
|
|
3184
3578
|
})
|
|
3185
3579
|
}
|
|
3186
3580
|
}
|
|
@@ -3209,8 +3603,8 @@ class Playwright extends Helper {
|
|
|
3209
3603
|
urls = [urls]
|
|
3210
3604
|
}
|
|
3211
3605
|
|
|
3212
|
-
urls.forEach(
|
|
3213
|
-
this.page.route(url,
|
|
3606
|
+
urls.forEach(url => {
|
|
3607
|
+
this.page.route(url, route => {
|
|
3214
3608
|
if (this.page.isClosed()) {
|
|
3215
3609
|
// Sometimes it happens that browser has been closed in the meantime.
|
|
3216
3610
|
// In this case we just don't fulfill to prevent error in test scenario.
|
|
@@ -3256,13 +3650,10 @@ class Playwright extends Helper {
|
|
|
3256
3650
|
*/
|
|
3257
3651
|
grabTrafficUrl(urlMatch) {
|
|
3258
3652
|
if (!this.recordedAtLeastOnce) {
|
|
3259
|
-
throw new Error(
|
|
3260
|
-
'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.',
|
|
3261
|
-
)
|
|
3653
|
+
throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.')
|
|
3262
3654
|
}
|
|
3263
3655
|
|
|
3264
3656
|
for (const i in this.requests) {
|
|
3265
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
3266
3657
|
if (this.requests.hasOwnProperty(i)) {
|
|
3267
3658
|
const request = this.requests[i]
|
|
3268
3659
|
|
|
@@ -3312,15 +3703,15 @@ class Playwright extends Helper {
|
|
|
3312
3703
|
await this.cdpSession.send('Network.enable')
|
|
3313
3704
|
await this.cdpSession.send('Page.enable')
|
|
3314
3705
|
|
|
3315
|
-
this.cdpSession.on('Network.webSocketFrameReceived',
|
|
3706
|
+
this.cdpSession.on('Network.webSocketFrameReceived', payload => {
|
|
3316
3707
|
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
|
|
3317
3708
|
})
|
|
3318
3709
|
|
|
3319
|
-
this.cdpSession.on('Network.webSocketFrameSent',
|
|
3710
|
+
this.cdpSession.on('Network.webSocketFrameSent', payload => {
|
|
3320
3711
|
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
|
|
3321
3712
|
})
|
|
3322
3713
|
|
|
3323
|
-
this.cdpSession.on('Network.webSocketFrameError',
|
|
3714
|
+
this.cdpSession.on('Network.webSocketFrameError', payload => {
|
|
3324
3715
|
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
|
|
3325
3716
|
})
|
|
3326
3717
|
}
|
|
@@ -3344,9 +3735,7 @@ class Playwright extends Helper {
|
|
|
3344
3735
|
grabWebSocketMessages() {
|
|
3345
3736
|
if (!this.recordingWebSocketMessages) {
|
|
3346
3737
|
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
3347
|
-
throw new Error(
|
|
3348
|
-
'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
|
|
3349
|
-
)
|
|
3738
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
|
|
3350
3739
|
}
|
|
3351
3740
|
}
|
|
3352
3741
|
return this.webSocketMessages
|
|
@@ -3441,48 +3830,7 @@ class Playwright extends Helper {
|
|
|
3441
3830
|
}
|
|
3442
3831
|
}
|
|
3443
3832
|
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
function buildLocatorString(locator) {
|
|
3447
|
-
if (locator.isCustom()) {
|
|
3448
|
-
return `${locator.type}=${locator.value}`
|
|
3449
|
-
}
|
|
3450
|
-
if (locator.isXPath()) {
|
|
3451
|
-
return `xpath=${locator.value}`
|
|
3452
|
-
}
|
|
3453
|
-
return locator.simplify()
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
async function findElements(matcher, locator) {
|
|
3457
|
-
if (locator.react) return findReact(matcher, locator)
|
|
3458
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
3459
|
-
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3460
|
-
locator = new Locator(locator, 'css')
|
|
3461
|
-
|
|
3462
|
-
return matcher.locator(buildLocatorString(locator)).all()
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
async function findElement(matcher, locator) {
|
|
3466
|
-
if (locator.react) return findReact(matcher, locator)
|
|
3467
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
3468
|
-
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3469
|
-
locator = new Locator(locator, 'css')
|
|
3470
|
-
|
|
3471
|
-
return matcher.locator(buildLocatorString(locator)).first()
|
|
3472
|
-
}
|
|
3473
|
-
|
|
3474
|
-
async function getVisibleElements(elements) {
|
|
3475
|
-
const visibleElements = []
|
|
3476
|
-
for (const element of elements) {
|
|
3477
|
-
if (await element.isVisible()) {
|
|
3478
|
-
visibleElements.push(element)
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
if (visibleElements.length === 0) {
|
|
3482
|
-
return elements
|
|
3483
|
-
}
|
|
3484
|
-
return visibleElements
|
|
3485
|
-
}
|
|
3833
|
+
export default Playwright
|
|
3486
3834
|
|
|
3487
3835
|
async function proceedClick(locator, context = null, options = {}) {
|
|
3488
3836
|
let matcher = await this._getContext()
|
|
@@ -3493,17 +3841,13 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3493
3841
|
}
|
|
3494
3842
|
const els = await findClickable.call(this, matcher, locator)
|
|
3495
3843
|
if (context) {
|
|
3496
|
-
assertElementExists(
|
|
3497
|
-
els,
|
|
3498
|
-
locator,
|
|
3499
|
-
'Clickable element',
|
|
3500
|
-
`was not found inside element ${new Locator(context).toString()}`,
|
|
3501
|
-
)
|
|
3844
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
3502
3845
|
} else {
|
|
3503
3846
|
assertElementExists(els, locator, 'Clickable element')
|
|
3504
3847
|
}
|
|
3505
3848
|
|
|
3506
3849
|
await highlightActiveElement.call(this, els[0])
|
|
3850
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
|
|
3507
3851
|
|
|
3508
3852
|
/*
|
|
3509
3853
|
using the force true options itself but instead dispatching a click
|
|
@@ -3524,15 +3868,26 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3524
3868
|
}
|
|
3525
3869
|
|
|
3526
3870
|
async function findClickable(matcher, locator) {
|
|
3527
|
-
|
|
3528
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
3529
|
-
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3871
|
+
const matchedLocator = new Locator(locator)
|
|
3530
3872
|
|
|
3531
|
-
|
|
3532
|
-
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
|
|
3873
|
+
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
|
|
3533
3874
|
|
|
3534
3875
|
let els
|
|
3535
|
-
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
|
+
}
|
|
3536
3891
|
|
|
3537
3892
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
3538
3893
|
if (els.length) return els
|
|
@@ -3547,7 +3902,7 @@ async function findClickable(matcher, locator) {
|
|
|
3547
3902
|
// Do nothing
|
|
3548
3903
|
}
|
|
3549
3904
|
|
|
3550
|
-
return findElements.call(this, matcher,
|
|
3905
|
+
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
|
|
3551
3906
|
}
|
|
3552
3907
|
|
|
3553
3908
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -3565,16 +3920,18 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
3565
3920
|
description = `element ${locator.toString()}`
|
|
3566
3921
|
const els = await this._locate(locator)
|
|
3567
3922
|
assertElementExists(els, locator.toString())
|
|
3568
|
-
allText = await Promise.all(els.map(
|
|
3923
|
+
allText = await Promise.all(els.map(el => el.innerText()))
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
if (store?.currentStep?.opts?.ignoreCase === true) {
|
|
3927
|
+
text = text.toLowerCase()
|
|
3928
|
+
allText = allText.map(elText => elText.toLowerCase())
|
|
3569
3929
|
}
|
|
3570
3930
|
|
|
3571
3931
|
if (strict) {
|
|
3572
|
-
return allText.map(
|
|
3932
|
+
return allText.map(elText => equals(description)[assertType](text, elText))
|
|
3573
3933
|
}
|
|
3574
|
-
return stringIncludes(description)[assertType](
|
|
3575
|
-
normalizeSpacesInString(text),
|
|
3576
|
-
normalizeSpacesInString(allText.join(' | ')),
|
|
3577
|
-
)
|
|
3934
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
|
|
3578
3935
|
}
|
|
3579
3936
|
|
|
3580
3937
|
async function findCheckable(locator, context) {
|
|
@@ -3586,10 +3943,10 @@ async function findCheckable(locator, context) {
|
|
|
3586
3943
|
|
|
3587
3944
|
const matchedLocator = new Locator(locator)
|
|
3588
3945
|
if (!matchedLocator.isFuzzy()) {
|
|
3589
|
-
return findElements.call(this, contextEl, matchedLocator
|
|
3946
|
+
return findElements.call(this, contextEl, matchedLocator)
|
|
3590
3947
|
}
|
|
3591
3948
|
|
|
3592
|
-
const literal = xpathLocator.literal(
|
|
3949
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3593
3950
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3594
3951
|
if (els.length) {
|
|
3595
3952
|
return els
|
|
@@ -3598,13 +3955,13 @@ async function findCheckable(locator, context) {
|
|
|
3598
3955
|
if (els.length) {
|
|
3599
3956
|
return els
|
|
3600
3957
|
}
|
|
3601
|
-
return findElements.call(this, contextEl,
|
|
3958
|
+
return findElements.call(this, contextEl, matchedLocator.value)
|
|
3602
3959
|
}
|
|
3603
3960
|
|
|
3604
3961
|
async function proceedIsChecked(assertType, option) {
|
|
3605
3962
|
let els = await findCheckable.call(this, option)
|
|
3606
3963
|
assertElementExists(els, option, 'Checkable')
|
|
3607
|
-
els = await Promise.all(els.map(
|
|
3964
|
+
els = await Promise.all(els.map(el => el.isChecked()))
|
|
3608
3965
|
const selected = els.reduce((prev, cur) => prev || cur)
|
|
3609
3966
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
3610
3967
|
}
|
|
@@ -3636,10 +3993,10 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3636
3993
|
const els = await findFields.call(this, field)
|
|
3637
3994
|
assertElementExists(els, field, 'Field')
|
|
3638
3995
|
const el = els[0]
|
|
3639
|
-
const tag = await el.evaluate(
|
|
3996
|
+
const tag = await el.evaluate(e => e.tagName)
|
|
3640
3997
|
const fieldType = await el.getAttribute('type')
|
|
3641
3998
|
|
|
3642
|
-
const proceedMultiple = async
|
|
3999
|
+
const proceedMultiple = async elements => {
|
|
3643
4000
|
const fields = Array.isArray(elements) ? elements : [elements]
|
|
3644
4001
|
|
|
3645
4002
|
const elementValues = []
|
|
@@ -3653,7 +4010,7 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3653
4010
|
if (assertType === 'assert') {
|
|
3654
4011
|
equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
|
|
3655
4012
|
}
|
|
3656
|
-
elementValues.forEach(
|
|
4013
|
+
elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
|
|
3657
4014
|
}
|
|
3658
4015
|
}
|
|
3659
4016
|
|
|
@@ -3740,6 +4097,8 @@ function isFrameLocator(locator) {
|
|
|
3740
4097
|
}
|
|
3741
4098
|
|
|
3742
4099
|
function assertElementExists(res, locator, prefix, suffix) {
|
|
4100
|
+
// if element text is an empty string, just exit this check
|
|
4101
|
+
if (typeof res === 'string' && res === '') return
|
|
3743
4102
|
if (!res || res.length === 0) {
|
|
3744
4103
|
throw new ElementNotFound(locator, prefix, suffix)
|
|
3745
4104
|
}
|
|
@@ -3776,12 +4135,9 @@ async function targetCreatedHandler(page) {
|
|
|
3776
4135
|
this.contextLocator = null
|
|
3777
4136
|
})
|
|
3778
4137
|
})
|
|
3779
|
-
page.on('console',
|
|
4138
|
+
page.on('console', msg => {
|
|
3780
4139
|
if (!consoleLogStore.includes(msg) && this.options.ignoreLog && !this.options.ignoreLog.includes(msg.type())) {
|
|
3781
|
-
this.debugSection(
|
|
3782
|
-
`Browser:${ucfirst(msg.type())}`,
|
|
3783
|
-
((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '),
|
|
3784
|
-
)
|
|
4140
|
+
this.debugSection(`Browser:${ucfirst(msg.type())}`, ((msg.text && msg.text()) || msg._text || '') + msg.args().join(' '))
|
|
3785
4141
|
}
|
|
3786
4142
|
consoleLogStore.add(msg)
|
|
3787
4143
|
})
|
|
@@ -3884,36 +4240,67 @@ async function clickablePoint(el) {
|
|
|
3884
4240
|
}
|
|
3885
4241
|
|
|
3886
4242
|
async function refreshContextSession() {
|
|
3887
|
-
// close other sessions
|
|
4243
|
+
// close other sessions with timeout protection, but preserve active session contexts
|
|
3888
4244
|
try {
|
|
3889
|
-
const contexts = await this.browser.contexts()
|
|
3890
|
-
|
|
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))
|
|
3891
4263
|
|
|
3892
|
-
|
|
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
|
+
}
|
|
3893
4267
|
} catch (e) {
|
|
3894
|
-
console.
|
|
4268
|
+
console.warn('Warning during context cleanup:', e.message)
|
|
3895
4269
|
}
|
|
3896
4270
|
|
|
3897
4271
|
if (this.page) {
|
|
3898
|
-
|
|
3899
|
-
|
|
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
|
+
}
|
|
3900
4278
|
}
|
|
3901
4279
|
|
|
3902
4280
|
if (this.options.keepBrowserState) return
|
|
3903
4281
|
|
|
3904
4282
|
if (!this.options.keepCookies) {
|
|
3905
4283
|
this.debugSection('Session', 'cleaning cookies and localStorage')
|
|
3906
|
-
|
|
4284
|
+
try {
|
|
4285
|
+
await this.clearCookie()
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
console.warn('Warning during cookie cleanup:', e.message)
|
|
4288
|
+
}
|
|
3907
4289
|
}
|
|
3908
|
-
const currentUrl = await this.grabCurrentUrl()
|
|
3909
4290
|
|
|
3910
|
-
|
|
3911
|
-
await this.
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
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)
|
|
3917
4304
|
}
|
|
3918
4305
|
}
|
|
3919
4306
|
|
|
@@ -3941,11 +4328,22 @@ async function saveTraceForContext(context, name) {
|
|
|
3941
4328
|
}
|
|
3942
4329
|
|
|
3943
4330
|
async function highlightActiveElement(element) {
|
|
3944
|
-
if (this.options.highlightElement &&
|
|
3945
|
-
await element.evaluate(
|
|
4331
|
+
if ((this.options.highlightElement || store.onPause) && store.debugMode) {
|
|
4332
|
+
await element.evaluate(el => {
|
|
3946
4333
|
const prevStyle = el.style.boxShadow
|
|
3947
|
-
el.style.boxShadow = '0px 0px 4px 3px rgba(
|
|
4334
|
+
el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
|
|
3948
4335
|
setTimeout(() => (el.style.boxShadow = prevStyle), 2000)
|
|
3949
4336
|
})
|
|
3950
4337
|
}
|
|
3951
4338
|
}
|
|
4339
|
+
|
|
4340
|
+
async function elToString(el, numberOfElements) {
|
|
4341
|
+
const html = await el.evaluate(node => node.outerHTML)
|
|
4342
|
+
return (
|
|
4343
|
+
html
|
|
4344
|
+
.replace(/\n/g, '')
|
|
4345
|
+
.replace(/\s+/g, ' ')
|
|
4346
|
+
.substring(0, 100 / numberOfElements)
|
|
4347
|
+
.trim() + '...'
|
|
4348
|
+
)
|
|
4349
|
+
}
|