codeceptjs 3.7.6-beta.4 → 4.0.0-beta.10.esm-aria
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/bin/codecept.js +51 -53
- package/bin/test-server.js +14 -3
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +15 -11
- package/lib/ai.js +72 -107
- package/lib/assert/empty.js +9 -8
- package/lib/assert/equal.js +15 -17
- package/lib/assert/error.js +2 -2
- package/lib/assert/include.js +9 -11
- package/lib/assert/throws.js +1 -1
- package/lib/assert/truth.js +8 -5
- package/lib/assert.js +18 -18
- package/lib/codecept.js +102 -75
- package/lib/colorUtils.js +48 -50
- package/lib/command/check.js +32 -27
- package/lib/command/configMigrate.js +11 -10
- package/lib/command/definitions.js +16 -10
- package/lib/command/dryRun.js +16 -16
- package/lib/command/generate.js +62 -27
- package/lib/command/gherkin/init.js +36 -38
- package/lib/command/gherkin/snippets.js +14 -14
- package/lib/command/gherkin/steps.js +21 -18
- package/lib/command/info.js +8 -8
- package/lib/command/init.js +36 -29
- package/lib/command/interactive.js +11 -10
- package/lib/command/list.js +10 -9
- package/lib/command/run-multiple/chunk.js +5 -5
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +3 -3
- package/lib/command/run-multiple.js +16 -13
- package/lib/command/run-rerun.js +6 -7
- package/lib/command/run-workers.js +24 -9
- package/lib/command/run.js +23 -8
- package/lib/command/utils.js +20 -18
- package/lib/command/workers/runTests.js +197 -114
- package/lib/config.js +124 -51
- package/lib/container.js +438 -87
- package/lib/data/context.js +6 -5
- package/lib/data/dataScenarioConfig.js +1 -1
- package/lib/data/dataTableArgument.js +1 -1
- package/lib/data/table.js +1 -1
- package/lib/effects.js +94 -10
- package/lib/element/WebElement.js +2 -2
- package/lib/els.js +11 -9
- package/lib/event.js +11 -10
- package/lib/globals.js +141 -0
- package/lib/heal.js +12 -12
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +50 -19
- package/lib/helper/Appium.js +19 -27
- package/lib/helper/FileSystem.js +32 -12
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +25 -29
- package/lib/helper/Mochawesome.js +7 -4
- package/lib/helper/Playwright.js +902 -164
- package/lib/helper/Puppeteer.js +383 -76
- package/lib/helper/REST.js +29 -12
- package/lib/helper/WebDriver.js +268 -61
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- 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/PlaywrightReactVueLocator.js +18 -9
- package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
- package/lib/helper/extras/Popup.js +1 -1
- package/lib/helper/extras/React.js +29 -30
- package/lib/helper/network/actions.js +29 -44
- 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 +19 -12
- package/lib/listener/emptyRun.js +6 -7
- package/lib/listener/enhancedGlobalRetry.js +6 -6
- package/lib/listener/exit.js +4 -3
- package/lib/listener/globalRetry.js +5 -5
- package/lib/listener/globalTimeout.js +30 -14
- package/lib/listener/helpers.js +39 -14
- package/lib/listener/mocha.js +3 -4
- package/lib/listener/result.js +4 -5
- package/lib/listener/retryEnhancer.js +3 -3
- package/lib/listener/steps.js +8 -7
- package/lib/listener/store.js +3 -3
- package/lib/locator.js +213 -192
- package/lib/mocha/asyncWrapper.js +105 -62
- package/lib/mocha/bdd.js +99 -13
- package/lib/mocha/cli.js +59 -26
- package/lib/mocha/factory.js +78 -19
- package/lib/mocha/featureConfig.js +1 -1
- package/lib/mocha/gherkin.js +56 -24
- package/lib/mocha/hooks.js +12 -3
- package/lib/mocha/index.js +13 -4
- package/lib/mocha/inject.js +22 -5
- package/lib/mocha/scenarioConfig.js +2 -2
- package/lib/mocha/suite.js +9 -2
- package/lib/mocha/test.js +10 -7
- package/lib/mocha/ui.js +28 -18
- package/lib/output.js +10 -8
- package/lib/parser.js +44 -44
- package/lib/pause.js +15 -16
- package/lib/plugin/analyze.js +19 -12
- package/lib/plugin/auth.js +20 -21
- package/lib/plugin/autoDelay.js +12 -8
- package/lib/plugin/coverage.js +28 -11
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +3 -2
- package/lib/plugin/enhancedRetryFailedStep.js +6 -6
- package/lib/plugin/heal.js +14 -9
- package/lib/plugin/htmlReporter.js +724 -99
- package/lib/plugin/pageInfo.js +10 -10
- package/lib/plugin/pauseOnFail.js +4 -3
- package/lib/plugin/retryFailedStep.js +48 -5
- package/lib/plugin/screenshotOnFail.js +75 -37
- package/lib/plugin/stepByStepReport.js +14 -14
- package/lib/plugin/stepTimeout.js +4 -3
- package/lib/plugin/subtitles.js +6 -5
- package/lib/recorder.js +33 -14
- package/lib/rerun.js +69 -26
- package/lib/result.js +4 -4
- package/lib/retryCoordinator.js +2 -2
- package/lib/secret.js +18 -17
- package/lib/session.js +95 -89
- package/lib/step/base.js +7 -7
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +1 -1
- package/lib/step/func.js +3 -3
- package/lib/step/helper.js +3 -3
- package/lib/step/meta.js +5 -5
- package/lib/step/record.js +11 -11
- package/lib/step/retry.js +3 -3
- package/lib/step/section.js +3 -3
- package/lib/step.js +7 -10
- package/lib/steps.js +9 -5
- package/lib/store.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +17 -6
- package/lib/timeout.js +1 -7
- package/lib/transform.js +8 -8
- package/lib/translation.js +32 -18
- package/lib/utils/mask_data.js +4 -10
- package/lib/utils.js +66 -64
- package/lib/workerStorage.js +17 -17
- package/lib/workers.js +214 -84
- package/package.json +41 -37
- package/translations/de-DE.js +2 -2
- package/translations/fr-FR.js +2 -2
- package/translations/index.js +23 -10
- package/translations/it-IT.js +2 -2
- package/translations/ja-JP.js +2 -2
- package/translations/nl-NL.js +2 -2
- package/translations/pl-PL.js +2 -2
- package/translations/pt-BR.js +2 -2
- package/translations/ru-RU.js +2 -2
- package/translations/utils.js +4 -3
- package/translations/zh-CN.js +2 -2
- package/translations/zh-TW.js +2 -2
- package/typings/index.d.ts +5 -3
- package/typings/promiseBasedTypes.d.ts +4 -0
- package/typings/types.d.ts +4 -0
- package/lib/helper/Nightmare.js +0 -1486
- package/lib/helper/Protractor.js +0 -1840
- package/lib/helper/TestCafe.js +0 -1391
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -61
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/autoLogin.js +0 -5
- package/lib/plugin/commentStep.js +0 -141
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -16
- package/lib/plugin/selenoid.js +0 -364
- package/lib/plugin/standardActingHelpers.js +0 -6
- package/lib/plugin/tryTo.js +0 -16
- package/lib/plugin/wdio.js +0 -247
- package/lib/within.js +0 -90
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const isElementClickable = require('./scripts/isElementClickable')
|
|
18
|
-
const {
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import fsExtra from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import Helper from '@codeceptjs/helper'
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
7
|
+
import promiseRetry from 'promise-retry'
|
|
8
|
+
import Locator from '../locator.js'
|
|
9
|
+
import recorder from '../recorder.js'
|
|
10
|
+
import store from '../store.js'
|
|
11
|
+
import { includes as stringIncludes } from '../assert/include.js'
|
|
12
|
+
import { urlEquals, equals } from '../assert/equal.js'
|
|
13
|
+
import { empty } from '../assert/empty.js'
|
|
14
|
+
import { truth } from '../assert/truth.js'
|
|
15
|
+
import isElementClickable from './scripts/isElementClickable.js'
|
|
16
|
+
import {
|
|
19
17
|
xpathLocator,
|
|
20
18
|
ucfirst,
|
|
21
19
|
fileExists,
|
|
@@ -28,19 +26,32 @@ const {
|
|
|
28
26
|
isModifierKey,
|
|
29
27
|
requireWithFallback,
|
|
30
28
|
normalizeSpacesInString,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
} from '../utils.js'
|
|
30
|
+
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
31
|
+
import ElementNotFound from './errors/ElementNotFound.js'
|
|
32
|
+
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
33
|
+
import Popup from './extras/Popup.js'
|
|
34
|
+
import Console from './extras/Console.js'
|
|
35
|
+
import { highlightElement } from './scripts/highlightElement.js'
|
|
36
|
+
import { blurElement } from './scripts/blurElement.js'
|
|
37
|
+
import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
|
|
38
|
+
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
39
|
+
import WebElement from '../element/WebElement.js'
|
|
42
40
|
|
|
43
41
|
let puppeteer
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wraps error objects that don't have a proper message property
|
|
45
|
+
* This is needed for ESM compatibility with Puppeteer error handling
|
|
46
|
+
*/
|
|
47
|
+
function wrapError(e) {
|
|
48
|
+
if (e && typeof e === 'object' && !e.message) {
|
|
49
|
+
const err = new Error(String(e))
|
|
50
|
+
err.stack = e.stack
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
return e
|
|
54
|
+
}
|
|
44
55
|
let perfTiming
|
|
45
56
|
const popupStore = new Popup()
|
|
46
57
|
const consoleLogStore = new Console()
|
|
@@ -214,7 +225,7 @@ class Puppeteer extends Helper {
|
|
|
214
225
|
constructor(config) {
|
|
215
226
|
super(config)
|
|
216
227
|
|
|
217
|
-
puppeteer
|
|
228
|
+
// puppeteer will be loaded dynamically in _init method
|
|
218
229
|
// set defaults
|
|
219
230
|
this.isRemoteBrowser = false
|
|
220
231
|
this.isRunning = false
|
|
@@ -294,13 +305,34 @@ class Puppeteer extends Helper {
|
|
|
294
305
|
|
|
295
306
|
static _checkRequirements() {
|
|
296
307
|
try {
|
|
297
|
-
|
|
308
|
+
// In ESM, puppeteer will be checked via dynamic import in _init
|
|
309
|
+
// The import will fail at module load time if puppeteer is missing
|
|
310
|
+
return null
|
|
298
311
|
} catch (e) {
|
|
299
312
|
return ['puppeteer']
|
|
300
313
|
}
|
|
301
314
|
}
|
|
302
315
|
|
|
303
|
-
_init() {
|
|
316
|
+
async _init() {
|
|
317
|
+
// Load puppeteer dynamically with fallback
|
|
318
|
+
if (!puppeteer) {
|
|
319
|
+
try {
|
|
320
|
+
const puppeteerModule = await import('puppeteer')
|
|
321
|
+
puppeteer = puppeteerModule.default || puppeteerModule
|
|
322
|
+
this.debugSection('Puppeteer', `Loaded puppeteer successfully, launch available: ${!!puppeteer.launch}`)
|
|
323
|
+
} catch (e) {
|
|
324
|
+
try {
|
|
325
|
+
const puppeteerModule = await import('puppeteer-core')
|
|
326
|
+
puppeteer = puppeteerModule.default || puppeteerModule
|
|
327
|
+
this.debugSection('Puppeteer', `Loaded puppeteer-core successfully, launch available: ${!!puppeteer.launch}`)
|
|
328
|
+
} catch (e2) {
|
|
329
|
+
throw new Error('Neither puppeteer nor puppeteer-core could be loaded. Please install one of them.')
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
this.debugSection('Puppeteer', `Puppeteer already loaded, launch available: ${!!puppeteer.launch}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
304
336
|
|
|
305
337
|
_beforeSuite() {
|
|
306
338
|
if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
|
|
@@ -404,9 +436,27 @@ class Puppeteer extends Helper {
|
|
|
404
436
|
} else {
|
|
405
437
|
this.activeSessionName = session
|
|
406
438
|
}
|
|
439
|
+
|
|
407
440
|
const defaultCtx = this.browser.defaultBrowserContext()
|
|
408
|
-
|
|
409
|
-
|
|
441
|
+
if (!defaultCtx) {
|
|
442
|
+
this.debug('Cannot restore session vars: default browser context is undefined')
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
448
|
+
if (existingPages && existingPages.length > 0) {
|
|
449
|
+
await this._setPage(await existingPages[0].page())
|
|
450
|
+
// Reset context-related variables to ensure clean state after session
|
|
451
|
+
this.context = await this.page
|
|
452
|
+
this.contextLocator = null
|
|
453
|
+
} else {
|
|
454
|
+
this.debug('Cannot restore session vars: no pages available')
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
this.debug(`Failed to restore session vars: ${err.message}`)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
410
460
|
|
|
411
461
|
return this._waitForAction()
|
|
412
462
|
},
|
|
@@ -566,6 +616,12 @@ class Puppeteer extends Helper {
|
|
|
566
616
|
}
|
|
567
617
|
|
|
568
618
|
async _startBrowser() {
|
|
619
|
+
this.debugSection('Puppeteer', `Starting browser. Puppeteer available: ${!!puppeteer}, launch available: ${!!puppeteer?.launch}`)
|
|
620
|
+
|
|
621
|
+
if (!puppeteer) {
|
|
622
|
+
throw new Error('Puppeteer is not loaded. Make sure _init() was called before _startBrowser()')
|
|
623
|
+
}
|
|
624
|
+
|
|
569
625
|
if (this.isRemoteBrowser) {
|
|
570
626
|
try {
|
|
571
627
|
this.browser = await puppeteer.connect(this.puppeteerOptions)
|
|
@@ -616,9 +672,14 @@ class Puppeteer extends Helper {
|
|
|
616
672
|
}
|
|
617
673
|
}
|
|
618
674
|
|
|
619
|
-
async _evaluateHandeInContext(...args) {
|
|
675
|
+
async _evaluateHandeInContext(fn, handle, ...args) {
|
|
676
|
+
// If handle is provided, evaluate directly on it to avoid "JavaScript world" errors
|
|
677
|
+
if (handle) {
|
|
678
|
+
return handle.evaluate(fn, ...args)
|
|
679
|
+
}
|
|
680
|
+
// Otherwise use the context
|
|
620
681
|
const context = await this._getContext()
|
|
621
|
-
return context.evaluateHandle(...args)
|
|
682
|
+
return context.evaluateHandle(fn, ...args)
|
|
622
683
|
}
|
|
623
684
|
|
|
624
685
|
async _withinBegin(locator) {
|
|
@@ -648,7 +709,11 @@ class Puppeteer extends Helper {
|
|
|
648
709
|
|
|
649
710
|
async _withinEnd() {
|
|
650
711
|
this.withinLocator = null
|
|
651
|
-
this.
|
|
712
|
+
if (this.page && !this.page.isClosed?.()) {
|
|
713
|
+
this.context = await this.page.mainFrame().$('body')
|
|
714
|
+
} else {
|
|
715
|
+
this.context = null
|
|
716
|
+
}
|
|
652
717
|
}
|
|
653
718
|
|
|
654
719
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
@@ -685,7 +750,21 @@ class Puppeteer extends Helper {
|
|
|
685
750
|
this.currentRunningTest.artifacts.trace = fileName
|
|
686
751
|
}
|
|
687
752
|
|
|
688
|
-
|
|
753
|
+
try {
|
|
754
|
+
await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
|
|
755
|
+
} catch (err) {
|
|
756
|
+
// Handle terminal navigation errors that shouldn't be retried
|
|
757
|
+
if (
|
|
758
|
+
err.message &&
|
|
759
|
+
(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('Navigation timeout'))
|
|
760
|
+
) {
|
|
761
|
+
// Mark this as a terminal error to prevent retries
|
|
762
|
+
const terminalError = new Error(err.message)
|
|
763
|
+
terminalError.isTerminal = true
|
|
764
|
+
throw terminalError
|
|
765
|
+
}
|
|
766
|
+
throw err
|
|
767
|
+
}
|
|
689
768
|
|
|
690
769
|
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
691
770
|
|
|
@@ -969,6 +1048,12 @@ class Puppeteer extends Helper {
|
|
|
969
1048
|
return new WebElement(elements[0], this)
|
|
970
1049
|
}
|
|
971
1050
|
|
|
1051
|
+
async grabWebElement(locator) {
|
|
1052
|
+
const els = await this._locate(locator)
|
|
1053
|
+
assertElementExists(els, locator)
|
|
1054
|
+
return els[0]
|
|
1055
|
+
}
|
|
1056
|
+
|
|
972
1057
|
/**
|
|
973
1058
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
974
1059
|
*
|
|
@@ -1130,7 +1215,7 @@ class Puppeteer extends Helper {
|
|
|
1130
1215
|
*
|
|
1131
1216
|
* {{ react }}
|
|
1132
1217
|
*/
|
|
1133
|
-
async click(locator, context = null) {
|
|
1218
|
+
async click(locator = '//body', context = null) {
|
|
1134
1219
|
return proceedClick.call(this, locator, context)
|
|
1135
1220
|
}
|
|
1136
1221
|
|
|
@@ -1290,13 +1375,64 @@ class Puppeteer extends Helper {
|
|
|
1290
1375
|
return proceedClick.call(this, locator, context, { button: 'right' })
|
|
1291
1376
|
}
|
|
1292
1377
|
|
|
1378
|
+
/**
|
|
1379
|
+
* Performs click at specific coordinates.
|
|
1380
|
+
* If locator is provided, the coordinates are relative to the element.
|
|
1381
|
+
* If locator is not provided, the coordinates are global page coordinates.
|
|
1382
|
+
*
|
|
1383
|
+
* ```js
|
|
1384
|
+
* // Click at global coordinates (100, 200)
|
|
1385
|
+
* I.clickXY(100, 200);
|
|
1386
|
+
*
|
|
1387
|
+
* // Click at coordinates (50, 30) relative to element
|
|
1388
|
+
* I.clickXY('#someElement', 50, 30);
|
|
1389
|
+
* ```
|
|
1390
|
+
*
|
|
1391
|
+
* @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
|
|
1392
|
+
* @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
|
|
1393
|
+
* @param {number} [y] Y coordinate relative to element.
|
|
1394
|
+
* @returns {Promise<void>}
|
|
1395
|
+
*/
|
|
1396
|
+
async clickXY(locator, x, y) {
|
|
1397
|
+
// If locator is a number, treat it as global X coordinate
|
|
1398
|
+
if (typeof locator === 'number') {
|
|
1399
|
+
const globalX = locator
|
|
1400
|
+
const globalY = x
|
|
1401
|
+
await this.page.mouse.click(globalX, globalY)
|
|
1402
|
+
return this._waitForAction()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Locator is provided, click relative to element
|
|
1406
|
+
const els = await this._locate(locator)
|
|
1407
|
+
assertElementExists(els, locator, 'Element to click')
|
|
1408
|
+
|
|
1409
|
+
const box = await els[0].boundingBox()
|
|
1410
|
+
if (!box) {
|
|
1411
|
+
throw new Error(`Element ${locator} is not visible or has no bounding box`)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const absoluteX = box.x + x
|
|
1415
|
+
const absoluteY = box.y + y
|
|
1416
|
+
|
|
1417
|
+
await this.page.mouse.click(absoluteX, absoluteY)
|
|
1418
|
+
return this._waitForAction()
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1293
1421
|
/**
|
|
1294
1422
|
* {{> checkOption }}
|
|
1295
1423
|
*/
|
|
1296
1424
|
async checkOption(field, context = null) {
|
|
1297
1425
|
const elm = await this._locateCheckable(field, context)
|
|
1298
|
-
|
|
1299
|
-
|
|
1426
|
+
let curentlyChecked = await elm
|
|
1427
|
+
.getProperty('checked')
|
|
1428
|
+
.then(checkedProperty => checkedProperty.jsonValue())
|
|
1429
|
+
.catch(() => null)
|
|
1430
|
+
|
|
1431
|
+
if (!curentlyChecked) {
|
|
1432
|
+
const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
|
|
1433
|
+
curentlyChecked = ariaChecked === 'true'
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1300
1436
|
if (!curentlyChecked) {
|
|
1301
1437
|
await elm.click()
|
|
1302
1438
|
return this._waitForAction()
|
|
@@ -1308,8 +1444,16 @@ class Puppeteer extends Helper {
|
|
|
1308
1444
|
*/
|
|
1309
1445
|
async uncheckOption(field, context = null) {
|
|
1310
1446
|
const elm = await this._locateCheckable(field, context)
|
|
1311
|
-
|
|
1312
|
-
|
|
1447
|
+
let curentlyChecked = await elm
|
|
1448
|
+
.getProperty('checked')
|
|
1449
|
+
.then(checkedProperty => checkedProperty.jsonValue())
|
|
1450
|
+
.catch(() => null)
|
|
1451
|
+
|
|
1452
|
+
if (!curentlyChecked) {
|
|
1453
|
+
const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
|
|
1454
|
+
curentlyChecked = ariaChecked === 'true'
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1313
1457
|
if (curentlyChecked) {
|
|
1314
1458
|
await elm.click()
|
|
1315
1459
|
return this._waitForAction()
|
|
@@ -1808,7 +1952,7 @@ class Puppeteer extends Helper {
|
|
|
1808
1952
|
*/
|
|
1809
1953
|
async grabHTMLFromAll(locator) {
|
|
1810
1954
|
const els = await this._locate(locator)
|
|
1811
|
-
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML
|
|
1955
|
+
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML)))
|
|
1812
1956
|
return values
|
|
1813
1957
|
}
|
|
1814
1958
|
|
|
@@ -1831,7 +1975,7 @@ class Puppeteer extends Helper {
|
|
|
1831
1975
|
*/
|
|
1832
1976
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1833
1977
|
const els = await this._locate(locator)
|
|
1834
|
-
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el)))
|
|
1978
|
+
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))))))
|
|
1835
1979
|
const cssValues = res.map(props => props[toCamelCase(cssProperty)])
|
|
1836
1980
|
|
|
1837
1981
|
return cssValues
|
|
@@ -1956,7 +2100,7 @@ class Puppeteer extends Helper {
|
|
|
1956
2100
|
const array = []
|
|
1957
2101
|
for (let index = 0; index < els.length; index++) {
|
|
1958
2102
|
const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
|
|
1959
|
-
array.push(
|
|
2103
|
+
array.push(a)
|
|
1960
2104
|
}
|
|
1961
2105
|
return array
|
|
1962
2106
|
}
|
|
@@ -1998,6 +2142,12 @@ class Puppeteer extends Helper {
|
|
|
1998
2142
|
|
|
1999
2143
|
this.debug(`Screenshot is saving to ${outputFile}`)
|
|
2000
2144
|
|
|
2145
|
+
// Safety check: ensure page exists and is not closed
|
|
2146
|
+
if (!this.page || this.page.isClosed?.()) {
|
|
2147
|
+
this.debugSection('Screenshot', 'Page is not available, skipping screenshot')
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2001
2151
|
await this.page.screenshot({
|
|
2002
2152
|
path: outputFile,
|
|
2003
2153
|
fullPage: fullPageOption,
|
|
@@ -2011,7 +2161,7 @@ class Puppeteer extends Helper {
|
|
|
2011
2161
|
|
|
2012
2162
|
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
|
|
2013
2163
|
|
|
2014
|
-
if (activeSessionPage) {
|
|
2164
|
+
if (activeSessionPage && !activeSessionPage.isClosed?.()) {
|
|
2015
2165
|
await activeSessionPage.screenshot({
|
|
2016
2166
|
path: outputFile,
|
|
2017
2167
|
fullPage: fullPageOption,
|
|
@@ -2730,7 +2880,7 @@ class Puppeteer extends Helper {
|
|
|
2730
2880
|
}
|
|
2731
2881
|
}
|
|
2732
2882
|
|
|
2733
|
-
|
|
2883
|
+
export default Puppeteer
|
|
2734
2884
|
|
|
2735
2885
|
/**
|
|
2736
2886
|
* Find elements using Puppeteer's native element discovery methods
|
|
@@ -2740,16 +2890,59 @@ module.exports = Puppeteer
|
|
|
2740
2890
|
* @returns {Promise<Array>} Array of ElementHandle objects
|
|
2741
2891
|
*/
|
|
2742
2892
|
async function findElements(matcher, locator) {
|
|
2743
|
-
if
|
|
2893
|
+
// Check if locator is a Locator object with react type, or a raw object with react property
|
|
2894
|
+
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
2895
|
+
if (isReactLocator) return findReactElements.call(this, locator)
|
|
2896
|
+
|
|
2744
2897
|
locator = new Locator(locator, 'css')
|
|
2898
|
+
|
|
2899
|
+
// Check if locator is a role locator and call findByRole
|
|
2900
|
+
if (locator.isRole()) return findByRole.call(this, matcher, locator)
|
|
2745
2901
|
|
|
2746
2902
|
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
|
|
2747
2903
|
if (!locator.isXPath()) return matcher.$$(locator.simplify())
|
|
2904
|
+
|
|
2748
2905
|
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2749
2906
|
if (puppeteer.default?.defaultBrowserRevision) {
|
|
2750
2907
|
return matcher.$$(`xpath/${locator.value}`)
|
|
2751
2908
|
}
|
|
2752
|
-
|
|
2909
|
+
|
|
2910
|
+
// For Puppeteer 24.x+, $x method was removed
|
|
2911
|
+
// Use ::-p-xpath() selector syntax
|
|
2912
|
+
// Check if matcher has $$ method (Page, Frame, or ElementHandle)
|
|
2913
|
+
if (matcher && typeof matcher.$$ === 'function') {
|
|
2914
|
+
const xpathSelector = `::-p-xpath(${locator.value})`
|
|
2915
|
+
try {
|
|
2916
|
+
return await matcher.$$(xpathSelector)
|
|
2917
|
+
} catch (e) {
|
|
2918
|
+
// XPath selector may not work on ElementHandle, fall through to evaluate method
|
|
2919
|
+
this.debug && this.debug(`XPath selector failed on ${matcher.constructor?.name}: ${e.message}`)
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// ElementHandles don't support XPath directly // Search within the element by making XPath relative
|
|
2924
|
+
try {
|
|
2925
|
+
const relativeXPath = locator.value.startsWith('.//') ? locator.value : `.//${locator.value.replace(/^\/\//, '')}`
|
|
2926
|
+
|
|
2927
|
+
// Use the element as context by evaluating XPath from it
|
|
2928
|
+
const elements = await matcher.evaluateHandle((element, xpath) => {
|
|
2929
|
+
const iterator = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
|
2930
|
+
const results = []
|
|
2931
|
+
for (let i = 0; i < iterator.snapshotLength; i++) {
|
|
2932
|
+
results.push(iterator.snapshotItem(i))
|
|
2933
|
+
}
|
|
2934
|
+
return results
|
|
2935
|
+
}, relativeXPath)
|
|
2936
|
+
|
|
2937
|
+
// Convert JSHandle to array of ElementHandles
|
|
2938
|
+
const properties = await elements.getProperties()
|
|
2939
|
+
return Array.from(properties.values())
|
|
2940
|
+
} catch (e) {
|
|
2941
|
+
this.debug(`XPath within element failed: ${e.message}`)
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// Fallback: return empty array
|
|
2945
|
+
return []
|
|
2753
2946
|
}
|
|
2754
2947
|
|
|
2755
2948
|
/**
|
|
@@ -2762,18 +2955,22 @@ async function findElements(matcher, locator) {
|
|
|
2762
2955
|
async function findElement(matcher, locator) {
|
|
2763
2956
|
if (locator.react) return findReactElements.call(this, locator)
|
|
2764
2957
|
locator = new Locator(locator, 'css')
|
|
2958
|
+
|
|
2959
|
+
// Check if locator is a role locator and call findByRole
|
|
2960
|
+
if (locator.isRole()) {
|
|
2961
|
+
const elements = await findByRole.call(this, matcher, locator)
|
|
2962
|
+
return elements[0]
|
|
2963
|
+
}
|
|
2765
2964
|
|
|
2766
2965
|
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
|
|
2767
2966
|
if (!locator.isXPath()) {
|
|
2768
2967
|
const elements = await matcher.$$(locator.simplify())
|
|
2769
2968
|
return elements[0]
|
|
2770
2969
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
}
|
|
2776
|
-
const elements = await matcher.$x(locator.value)
|
|
2970
|
+
|
|
2971
|
+
// For XPath in Puppeteer 24.x+, use the same approach as findElements
|
|
2972
|
+
// $x method was removed, so we use ::-p-xpath() or fallback
|
|
2973
|
+
const elements = await findElements.call(this, matcher, locator)
|
|
2777
2974
|
return elements[0]
|
|
2778
2975
|
}
|
|
2779
2976
|
|
|
@@ -2804,12 +3001,12 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2804
3001
|
}
|
|
2805
3002
|
|
|
2806
3003
|
async function findClickable(matcher, locator) {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (!
|
|
3004
|
+
const matchedLocator = new Locator(locator)
|
|
3005
|
+
|
|
3006
|
+
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
|
|
2810
3007
|
|
|
2811
3008
|
let els
|
|
2812
|
-
const literal = xpathLocator.literal(
|
|
3009
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2813
3010
|
|
|
2814
3011
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
2815
3012
|
if (els.length) return els
|
|
@@ -2824,7 +3021,15 @@ async function findClickable(matcher, locator) {
|
|
|
2824
3021
|
// Do nothing
|
|
2825
3022
|
}
|
|
2826
3023
|
|
|
2827
|
-
|
|
3024
|
+
// Try ARIA selector for accessible name
|
|
3025
|
+
try {
|
|
3026
|
+
els = await matcher.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3027
|
+
if (els.length) return els
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
// ARIA selector not supported or failed
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
|
|
2828
3033
|
}
|
|
2829
3034
|
|
|
2830
3035
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -2868,10 +3073,10 @@ async function findCheckable(locator, context) {
|
|
|
2868
3073
|
|
|
2869
3074
|
const matchedLocator = new Locator(locator)
|
|
2870
3075
|
if (!matchedLocator.isFuzzy()) {
|
|
2871
|
-
return findElements.call(this, contextEl, matchedLocator
|
|
3076
|
+
return findElements.call(this, contextEl, matchedLocator)
|
|
2872
3077
|
}
|
|
2873
3078
|
|
|
2874
|
-
const literal = xpathLocator.literal(
|
|
3079
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2875
3080
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
2876
3081
|
if (els.length) {
|
|
2877
3082
|
return els
|
|
@@ -2880,15 +3085,39 @@ async function findCheckable(locator, context) {
|
|
|
2880
3085
|
if (els.length) {
|
|
2881
3086
|
return els
|
|
2882
3087
|
}
|
|
2883
|
-
|
|
3088
|
+
|
|
3089
|
+
// Try ARIA selector for accessible name
|
|
3090
|
+
try {
|
|
3091
|
+
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3092
|
+
if (els.length) return els
|
|
3093
|
+
} catch (err) {
|
|
3094
|
+
// ARIA selector not supported or failed
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
return findElements.call(this, contextEl, matchedLocator.value)
|
|
2884
3098
|
}
|
|
2885
3099
|
|
|
2886
3100
|
async function proceedIsChecked(assertType, option) {
|
|
2887
3101
|
let els = await findCheckable.call(this, option)
|
|
2888
3102
|
assertElementExists(els, option, 'Checkable')
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3103
|
+
|
|
3104
|
+
const checkedStates = await Promise.all(
|
|
3105
|
+
els.map(async el => {
|
|
3106
|
+
const checked = await el
|
|
3107
|
+
.getProperty('checked')
|
|
3108
|
+
.then(p => p.jsonValue())
|
|
3109
|
+
.catch(() => null)
|
|
3110
|
+
|
|
3111
|
+
if (checked) {
|
|
3112
|
+
return checked
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
const ariaChecked = await el.evaluate(el => el.getAttribute('aria-checked'))
|
|
3116
|
+
return ariaChecked === 'true'
|
|
3117
|
+
}),
|
|
3118
|
+
)
|
|
3119
|
+
|
|
3120
|
+
const selected = checkedStates.reduce((prev, cur) => prev || cur)
|
|
2892
3121
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
2893
3122
|
}
|
|
2894
3123
|
|
|
@@ -2903,7 +3132,7 @@ async function findFields(locator) {
|
|
|
2903
3132
|
if (!matchedLocator.isFuzzy()) {
|
|
2904
3133
|
return this._locate(matchedLocator)
|
|
2905
3134
|
}
|
|
2906
|
-
const literal = xpathLocator.literal(
|
|
3135
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2907
3136
|
|
|
2908
3137
|
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
|
|
2909
3138
|
if (els.length) {
|
|
@@ -2918,7 +3147,17 @@ async function findFields(locator) {
|
|
|
2918
3147
|
if (els.length) {
|
|
2919
3148
|
return els
|
|
2920
3149
|
}
|
|
2921
|
-
|
|
3150
|
+
|
|
3151
|
+
// Try ARIA selector for accessible name
|
|
3152
|
+
try {
|
|
3153
|
+
const page = await this.context
|
|
3154
|
+
els = await page.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3155
|
+
if (els.length) return els
|
|
3156
|
+
} catch (err) {
|
|
3157
|
+
// ARIA selector not supported or failed
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
return this._locate({ css: matchedLocator.value })
|
|
2922
3161
|
}
|
|
2923
3162
|
|
|
2924
3163
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
@@ -2996,19 +3235,30 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2996
3235
|
}
|
|
2997
3236
|
return proceedMultiple(els[0])
|
|
2998
3237
|
}
|
|
2999
|
-
|
|
3238
|
+
|
|
3239
|
+
let fieldVal = await el.getProperty('value').then(el => el.jsonValue())
|
|
3240
|
+
|
|
3241
|
+
if (fieldVal === undefined || fieldVal === null) {
|
|
3242
|
+
fieldVal = await el.evaluate(el => el.textContent || el.innerText)
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3000
3245
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
|
|
3001
3246
|
}
|
|
3002
3247
|
|
|
3003
3248
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
3004
3249
|
const matches = []
|
|
3005
3250
|
for (const element of elements) {
|
|
3006
|
-
|
|
3251
|
+
let val = await element.getProperty('value').then(el => el.jsonValue())
|
|
3252
|
+
|
|
3253
|
+
if (val === undefined || val === null) {
|
|
3254
|
+
val = await element.evaluate(el => el.textContent || el.innerText)
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3007
3257
|
let isSelected = true
|
|
3008
3258
|
if (onlySelected) {
|
|
3009
3259
|
isSelected = await elementSelected(element)
|
|
3010
3260
|
}
|
|
3011
|
-
if ((value == null || val.indexOf(value) > -1) && isSelected) {
|
|
3261
|
+
if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
|
|
3012
3262
|
matches.push(element)
|
|
3013
3263
|
}
|
|
3014
3264
|
}
|
|
@@ -3170,7 +3420,14 @@ function _waitForElement(locator, options) {
|
|
|
3170
3420
|
}
|
|
3171
3421
|
}
|
|
3172
3422
|
|
|
3173
|
-
async function findReactElements(locator
|
|
3423
|
+
async function findReactElements(locator) {
|
|
3424
|
+
// Handle both Locator objects and raw locator objects
|
|
3425
|
+
const resolved = locator.locator ? locator.locator : toLocatorConfig(locator, 'react')
|
|
3426
|
+
this.debug(`Finding React elements: ${JSON.stringify(resolved)}`)
|
|
3427
|
+
|
|
3428
|
+
// Use createRequire to access require.resolve in ESM
|
|
3429
|
+
const { createRequire } = await import('module')
|
|
3430
|
+
const require = createRequire(import.meta.url)
|
|
3174
3431
|
const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
|
|
3175
3432
|
await this.page.evaluate(resqScript.toString())
|
|
3176
3433
|
|
|
@@ -3213,9 +3470,9 @@ async function findReactElements(locator, props = {}, state = {}) {
|
|
|
3213
3470
|
return [...nodes]
|
|
3214
3471
|
},
|
|
3215
3472
|
{
|
|
3216
|
-
selector:
|
|
3217
|
-
props:
|
|
3218
|
-
state:
|
|
3473
|
+
selector: resolved.react,
|
|
3474
|
+
props: resolved.props || {},
|
|
3475
|
+
state: resolved.state || {},
|
|
3219
3476
|
},
|
|
3220
3477
|
)
|
|
3221
3478
|
|
|
@@ -3231,3 +3488,53 @@ async function findReactElements(locator, props = {}, state = {}) {
|
|
|
3231
3488
|
await arrayHandle.dispose()
|
|
3232
3489
|
return result
|
|
3233
3490
|
}
|
|
3491
|
+
|
|
3492
|
+
async function findByRole(matcher, locator) {
|
|
3493
|
+
const resolved = toLocatorConfig(locator, 'role')
|
|
3494
|
+
const roleSelector = buildRoleSelector(resolved)
|
|
3495
|
+
|
|
3496
|
+
if (!resolved.text && !resolved.name) {
|
|
3497
|
+
return matcher.$$(roleSelector)
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
const allElements = await matcher.$$(roleSelector)
|
|
3501
|
+
const filtered = []
|
|
3502
|
+
const accessibleName = resolved.text ?? resolved.name
|
|
3503
|
+
const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
|
|
3504
|
+
|
|
3505
|
+
for (const el of allElements) {
|
|
3506
|
+
const texts = await el.evaluate(e => {
|
|
3507
|
+
const ariaLabel = e.hasAttribute('aria-label') ? e.getAttribute('aria-label') : ''
|
|
3508
|
+
const labelText = e.id ? document.querySelector(`label[for="${e.id}"]`)?.textContent.trim() || '' : ''
|
|
3509
|
+
const placeholder = e.getAttribute('placeholder') || ''
|
|
3510
|
+
const innerText = e.innerText ? e.innerText.trim() : ''
|
|
3511
|
+
return [ariaLabel || labelText, placeholder, innerText]
|
|
3512
|
+
})
|
|
3513
|
+
|
|
3514
|
+
if (texts.some(text => matcherFn(text))) filtered.push(el)
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
return filtered
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
function toLocatorConfig(locator, key) {
|
|
3521
|
+
const matchedLocator = new Locator(locator, key)
|
|
3522
|
+
if (matchedLocator.locator) return matchedLocator.locator
|
|
3523
|
+
return { [key]: matchedLocator.value }
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
function buildRoleSelector(resolved) {
|
|
3527
|
+
return `::-p-aria([role="${resolved.role}"])`
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
function createRoleTextMatcher(expected, exactMatch) {
|
|
3531
|
+
if (expected instanceof RegExp) {
|
|
3532
|
+
return value => expected.test(value || '')
|
|
3533
|
+
}
|
|
3534
|
+
const target = String(expected)
|
|
3535
|
+
if (exactMatch) {
|
|
3536
|
+
return value => value === target
|
|
3537
|
+
}
|
|
3538
|
+
return value => typeof value === 'string' && value.includes(target)
|
|
3539
|
+
}
|
|
3540
|
+
|