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/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,29 +26,31 @@ const {
|
|
|
28
26
|
isModifierKey,
|
|
29
27
|
requireWithFallback,
|
|
30
28
|
normalizeSpacesInString,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
seeElementError,
|
|
42
|
-
dontSeeElementInDOMError,
|
|
43
|
-
seeElementInDOMError,
|
|
44
|
-
} = require('./errors/ElementAssertion')
|
|
45
|
-
const {
|
|
46
|
-
dontSeeTraffic,
|
|
47
|
-
seeTraffic,
|
|
48
|
-
grabRecordedNetworkTraffics,
|
|
49
|
-
stopRecordingTraffic,
|
|
50
|
-
flushNetworkTraffics,
|
|
51
|
-
} = require('./network/actions')
|
|
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'
|
|
52
39
|
|
|
53
40
|
let puppeteer
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wraps error objects that don't have a proper message property
|
|
44
|
+
* This is needed for ESM compatibility with Puppeteer error handling
|
|
45
|
+
*/
|
|
46
|
+
function wrapError(e) {
|
|
47
|
+
if (e && typeof e === 'object' && !e.message) {
|
|
48
|
+
const err = new Error(String(e))
|
|
49
|
+
err.stack = e.stack
|
|
50
|
+
return err
|
|
51
|
+
}
|
|
52
|
+
return e
|
|
53
|
+
}
|
|
54
54
|
let perfTiming
|
|
55
55
|
const popupStore = new Popup()
|
|
56
56
|
const consoleLogStore = new Console()
|
|
@@ -224,7 +224,7 @@ class Puppeteer extends Helper {
|
|
|
224
224
|
constructor(config) {
|
|
225
225
|
super(config)
|
|
226
226
|
|
|
227
|
-
puppeteer
|
|
227
|
+
// puppeteer will be loaded dynamically in _init method
|
|
228
228
|
// set defaults
|
|
229
229
|
this.isRemoteBrowser = false
|
|
230
230
|
this.isRunning = false
|
|
@@ -271,9 +271,7 @@ class Puppeteer extends Helper {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
_getOptions(config) {
|
|
274
|
-
return config.browser === 'firefox'
|
|
275
|
-
? Object.assign(this.options.firefox, { product: 'firefox' })
|
|
276
|
-
: this.options.chrome
|
|
274
|
+
return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome
|
|
277
275
|
}
|
|
278
276
|
|
|
279
277
|
_setConfig(config) {
|
|
@@ -306,13 +304,34 @@ class Puppeteer extends Helper {
|
|
|
306
304
|
|
|
307
305
|
static _checkRequirements() {
|
|
308
306
|
try {
|
|
309
|
-
|
|
307
|
+
// In ESM, puppeteer will be checked via dynamic import in _init
|
|
308
|
+
// The import will fail at module load time if puppeteer is missing
|
|
309
|
+
return null
|
|
310
310
|
} catch (e) {
|
|
311
311
|
return ['puppeteer']
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
_init() {
|
|
315
|
+
async _init() {
|
|
316
|
+
// Load puppeteer dynamically with fallback
|
|
317
|
+
if (!puppeteer) {
|
|
318
|
+
try {
|
|
319
|
+
const puppeteerModule = await import('puppeteer')
|
|
320
|
+
puppeteer = puppeteerModule.default || puppeteerModule
|
|
321
|
+
this.debugSection('Puppeteer', `Loaded puppeteer successfully, launch available: ${!!puppeteer.launch}`)
|
|
322
|
+
} catch (e) {
|
|
323
|
+
try {
|
|
324
|
+
const puppeteerModule = await import('puppeteer-core')
|
|
325
|
+
puppeteer = puppeteerModule.default || puppeteerModule
|
|
326
|
+
this.debugSection('Puppeteer', `Loaded puppeteer-core successfully, launch available: ${!!puppeteer.launch}`)
|
|
327
|
+
} catch (e2) {
|
|
328
|
+
throw new Error('Neither puppeteer nor puppeteer-core could be loaded. Please install one of them.')
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
this.debugSection('Puppeteer', `Puppeteer already loaded, launch available: ${!!puppeteer.launch}`)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
316
335
|
|
|
317
336
|
_beforeSuite() {
|
|
318
337
|
if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
|
|
@@ -325,8 +344,8 @@ class Puppeteer extends Helper {
|
|
|
325
344
|
this.sessionPages = {}
|
|
326
345
|
this.currentRunningTest = test
|
|
327
346
|
recorder.retry({
|
|
328
|
-
retries:
|
|
329
|
-
when:
|
|
347
|
+
retries: test?.opts?.conditionalRetries || 3,
|
|
348
|
+
when: err => {
|
|
330
349
|
if (!err || typeof err.message !== 'string') {
|
|
331
350
|
return false
|
|
332
351
|
}
|
|
@@ -346,7 +365,7 @@ class Puppeteer extends Helper {
|
|
|
346
365
|
const contexts = this.browser.browserContexts()
|
|
347
366
|
const defaultCtx = contexts.shift()
|
|
348
367
|
|
|
349
|
-
await Promise.all(contexts.map(
|
|
368
|
+
await Promise.all(contexts.map(c => c.close()))
|
|
350
369
|
|
|
351
370
|
if (this.options.restart) {
|
|
352
371
|
this.isRunning = false
|
|
@@ -355,7 +374,7 @@ class Puppeteer extends Helper {
|
|
|
355
374
|
|
|
356
375
|
// ensure this.page is from default context
|
|
357
376
|
if (this.page) {
|
|
358
|
-
const existingPages = defaultCtx.targets().filter(
|
|
377
|
+
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
359
378
|
await this._setPage(await existingPages[0].page())
|
|
360
379
|
}
|
|
361
380
|
|
|
@@ -368,10 +387,10 @@ class Puppeteer extends Helper {
|
|
|
368
387
|
const currentUrl = await this.grabCurrentUrl()
|
|
369
388
|
|
|
370
389
|
if (currentUrl.startsWith('http')) {
|
|
371
|
-
await this.executeScript('localStorage.clear();').catch(
|
|
390
|
+
await this.executeScript('localStorage.clear();').catch(err => {
|
|
372
391
|
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
373
392
|
})
|
|
374
|
-
await this.executeScript('sessionStorage.clear();').catch(
|
|
393
|
+
await this.executeScript('sessionStorage.clear();').catch(err => {
|
|
375
394
|
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
376
395
|
})
|
|
377
396
|
}
|
|
@@ -400,12 +419,12 @@ class Puppeteer extends Helper {
|
|
|
400
419
|
stop: async () => {
|
|
401
420
|
// is closed by _after
|
|
402
421
|
},
|
|
403
|
-
loadVars: async
|
|
404
|
-
const existingPages = context.targets().filter(
|
|
422
|
+
loadVars: async context => {
|
|
423
|
+
const existingPages = context.targets().filter(t => t.type() === 'page')
|
|
405
424
|
this.sessionPages[this.activeSessionName] = await existingPages[0].page()
|
|
406
425
|
return this._setPage(this.sessionPages[this.activeSessionName])
|
|
407
426
|
},
|
|
408
|
-
restoreVars: async
|
|
427
|
+
restoreVars: async session => {
|
|
409
428
|
this.withinLocator = null
|
|
410
429
|
|
|
411
430
|
if (!session) {
|
|
@@ -414,7 +433,7 @@ class Puppeteer extends Helper {
|
|
|
414
433
|
this.activeSessionName = session
|
|
415
434
|
}
|
|
416
435
|
const defaultCtx = this.browser.defaultBrowserContext()
|
|
417
|
-
const existingPages = defaultCtx.targets().filter(
|
|
436
|
+
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
418
437
|
await this._setPage(await existingPages[0].page())
|
|
419
438
|
|
|
420
439
|
return this._waitForAction()
|
|
@@ -517,7 +536,7 @@ class Puppeteer extends Helper {
|
|
|
517
536
|
if (!page) {
|
|
518
537
|
return
|
|
519
538
|
}
|
|
520
|
-
page.on('error', async
|
|
539
|
+
page.on('error', async error => {
|
|
521
540
|
console.error('Puppeteer page error', error)
|
|
522
541
|
})
|
|
523
542
|
}
|
|
@@ -533,7 +552,7 @@ class Puppeteer extends Helper {
|
|
|
533
552
|
if (!page) {
|
|
534
553
|
return
|
|
535
554
|
}
|
|
536
|
-
page.on('dialog', async
|
|
555
|
+
page.on('dialog', async dialog => {
|
|
537
556
|
popupStore.popup = dialog
|
|
538
557
|
const action = popupStore.actionType || this.options.defaultPopupAction
|
|
539
558
|
await this._waitForAction()
|
|
@@ -575,6 +594,12 @@ class Puppeteer extends Helper {
|
|
|
575
594
|
}
|
|
576
595
|
|
|
577
596
|
async _startBrowser() {
|
|
597
|
+
this.debugSection('Puppeteer', `Starting browser. Puppeteer available: ${!!puppeteer}, launch available: ${!!puppeteer?.launch}`)
|
|
598
|
+
|
|
599
|
+
if (!puppeteer) {
|
|
600
|
+
throw new Error('Puppeteer is not loaded. Make sure _init() was called before _startBrowser()')
|
|
601
|
+
}
|
|
602
|
+
|
|
578
603
|
if (this.isRemoteBrowser) {
|
|
579
604
|
try {
|
|
580
605
|
this.browser = await puppeteer.connect(this.puppeteerOptions)
|
|
@@ -588,15 +613,15 @@ class Puppeteer extends Helper {
|
|
|
588
613
|
this.browser = await puppeteer.launch(this.puppeteerOptions)
|
|
589
614
|
}
|
|
590
615
|
|
|
591
|
-
this.browser.on('targetcreated',
|
|
616
|
+
this.browser.on('targetcreated', target =>
|
|
592
617
|
target
|
|
593
618
|
.page()
|
|
594
|
-
.then(
|
|
595
|
-
.catch(
|
|
619
|
+
.then(page => targetCreatedHandler.call(this, page))
|
|
620
|
+
.catch(e => {
|
|
596
621
|
console.error('Puppeteer page error', e)
|
|
597
622
|
}),
|
|
598
623
|
)
|
|
599
|
-
this.browser.on('targetchanged',
|
|
624
|
+
this.browser.on('targetchanged', target => {
|
|
600
625
|
this.debugSection('Url', target.url())
|
|
601
626
|
})
|
|
602
627
|
|
|
@@ -639,9 +664,7 @@ class Puppeteer extends Helper {
|
|
|
639
664
|
|
|
640
665
|
if (frame) {
|
|
641
666
|
if (Array.isArray(frame)) {
|
|
642
|
-
return this.switchTo(null).then(() =>
|
|
643
|
-
frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()),
|
|
644
|
-
)
|
|
667
|
+
return this.switchTo(null).then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()))
|
|
645
668
|
}
|
|
646
669
|
await this.switchTo(frame)
|
|
647
670
|
this.withinLocator = new Locator(frame)
|
|
@@ -664,7 +687,7 @@ class Puppeteer extends Helper {
|
|
|
664
687
|
const navigationStart = timing.navigationStart
|
|
665
688
|
|
|
666
689
|
const extractedData = {}
|
|
667
|
-
dataNames.forEach(
|
|
690
|
+
dataNames.forEach(name => {
|
|
668
691
|
extractedData[name] = timing[name] - navigationStart
|
|
669
692
|
})
|
|
670
693
|
|
|
@@ -694,17 +717,25 @@ class Puppeteer extends Helper {
|
|
|
694
717
|
this.currentRunningTest.artifacts.trace = fileName
|
|
695
718
|
}
|
|
696
719
|
|
|
697
|
-
|
|
720
|
+
try {
|
|
721
|
+
await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
|
|
722
|
+
} catch (err) {
|
|
723
|
+
// Handle terminal navigation errors that shouldn't be retried
|
|
724
|
+
if (
|
|
725
|
+
err.message &&
|
|
726
|
+
(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'))
|
|
727
|
+
) {
|
|
728
|
+
// Mark this as a terminal error to prevent retries
|
|
729
|
+
const terminalError = new Error(err.message)
|
|
730
|
+
terminalError.isTerminal = true
|
|
731
|
+
throw terminalError
|
|
732
|
+
}
|
|
733
|
+
throw err
|
|
734
|
+
}
|
|
698
735
|
|
|
699
736
|
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
700
737
|
|
|
701
|
-
perfTiming = this._extractDataFromPerformanceTiming(
|
|
702
|
-
performanceTiming,
|
|
703
|
-
'responseEnd',
|
|
704
|
-
'domInteractive',
|
|
705
|
-
'domContentLoadedEventEnd',
|
|
706
|
-
'loadEventEnd',
|
|
707
|
-
)
|
|
738
|
+
perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
|
|
708
739
|
|
|
709
740
|
return this._waitForAction()
|
|
710
741
|
}
|
|
@@ -815,10 +846,7 @@ class Puppeteer extends Helper {
|
|
|
815
846
|
return this.executeScript(() => {
|
|
816
847
|
const body = document.body
|
|
817
848
|
const html = document.documentElement
|
|
818
|
-
window.scrollTo(
|
|
819
|
-
0,
|
|
820
|
-
Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
|
|
821
|
-
)
|
|
849
|
+
window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
|
|
822
850
|
})
|
|
823
851
|
}
|
|
824
852
|
|
|
@@ -836,13 +864,9 @@ class Puppeteer extends Helper {
|
|
|
836
864
|
const els = await this._locate(locator)
|
|
837
865
|
assertElementExists(els, locator, 'Element')
|
|
838
866
|
const el = els[0]
|
|
839
|
-
await el.evaluate(
|
|
867
|
+
await el.evaluate(el => el.scrollIntoView())
|
|
840
868
|
const elementCoordinates = await getClickablePoint(els[0])
|
|
841
|
-
await this.executeScript(
|
|
842
|
-
(x, y) => window.scrollBy(x, y),
|
|
843
|
-
elementCoordinates.x + offsetX,
|
|
844
|
-
elementCoordinates.y + offsetY,
|
|
845
|
-
)
|
|
869
|
+
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
|
|
846
870
|
} else {
|
|
847
871
|
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
|
|
848
872
|
}
|
|
@@ -955,6 +979,12 @@ class Puppeteer extends Helper {
|
|
|
955
979
|
return this._locate(locator)
|
|
956
980
|
}
|
|
957
981
|
|
|
982
|
+
async grabWebElement(locator) {
|
|
983
|
+
const els = await this._locate(locator)
|
|
984
|
+
assertElementExists(els, locator)
|
|
985
|
+
return els[0]
|
|
986
|
+
}
|
|
987
|
+
|
|
958
988
|
/**
|
|
959
989
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
960
990
|
*
|
|
@@ -1025,10 +1055,10 @@ class Puppeteer extends Helper {
|
|
|
1025
1055
|
*/
|
|
1026
1056
|
async closeOtherTabs() {
|
|
1027
1057
|
const pages = await this.browser.pages()
|
|
1028
|
-
const otherPages = pages.filter(
|
|
1058
|
+
const otherPages = pages.filter(page => page !== this.page)
|
|
1029
1059
|
|
|
1030
1060
|
let p = Promise.resolve()
|
|
1031
|
-
otherPages.forEach(
|
|
1061
|
+
otherPages.forEach(page => {
|
|
1032
1062
|
p = p.then(() => page.close())
|
|
1033
1063
|
})
|
|
1034
1064
|
await p
|
|
@@ -1061,19 +1091,11 @@ class Puppeteer extends Helper {
|
|
|
1061
1091
|
*/
|
|
1062
1092
|
async seeElement(locator) {
|
|
1063
1093
|
let els = await this._locate(locator)
|
|
1064
|
-
els = (await Promise.all(els.map(
|
|
1094
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1065
1095
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1066
|
-
els = await Promise.all(
|
|
1067
|
-
els.map(
|
|
1068
|
-
async (el) =>
|
|
1069
|
-
(await el.evaluate(
|
|
1070
|
-
(node) =>
|
|
1071
|
-
window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none',
|
|
1072
|
-
)) && el,
|
|
1073
|
-
),
|
|
1074
|
-
)
|
|
1096
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1075
1097
|
try {
|
|
1076
|
-
return empty('visible elements').negate(els.filter(
|
|
1098
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1077
1099
|
} catch (e) {
|
|
1078
1100
|
dontSeeElementError(locator)
|
|
1079
1101
|
}
|
|
@@ -1085,19 +1107,11 @@ class Puppeteer extends Helper {
|
|
|
1085
1107
|
*/
|
|
1086
1108
|
async dontSeeElement(locator) {
|
|
1087
1109
|
let els = await this._locate(locator)
|
|
1088
|
-
els = (await Promise.all(els.map(
|
|
1110
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1089
1111
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1090
|
-
els = await Promise.all(
|
|
1091
|
-
els.map(
|
|
1092
|
-
async (el) =>
|
|
1093
|
-
(await el.evaluate(
|
|
1094
|
-
(node) =>
|
|
1095
|
-
window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none',
|
|
1096
|
-
)) && el,
|
|
1097
|
-
),
|
|
1098
|
-
)
|
|
1112
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1099
1113
|
try {
|
|
1100
|
-
return empty('visible elements').assert(els.filter(
|
|
1114
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1101
1115
|
} catch (e) {
|
|
1102
1116
|
seeElementError(locator)
|
|
1103
1117
|
}
|
|
@@ -1109,7 +1123,7 @@ class Puppeteer extends Helper {
|
|
|
1109
1123
|
async seeElementInDOM(locator) {
|
|
1110
1124
|
const els = await this._locate(locator)
|
|
1111
1125
|
try {
|
|
1112
|
-
return empty('elements on page').negate(els.filter(
|
|
1126
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1113
1127
|
} catch (e) {
|
|
1114
1128
|
dontSeeElementInDOMError(locator)
|
|
1115
1129
|
}
|
|
@@ -1121,7 +1135,7 @@ class Puppeteer extends Helper {
|
|
|
1121
1135
|
async dontSeeElementInDOM(locator) {
|
|
1122
1136
|
const els = await this._locate(locator)
|
|
1123
1137
|
try {
|
|
1124
|
-
return empty('elements on a page').assert(els.filter(
|
|
1138
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1125
1139
|
} catch (e) {
|
|
1126
1140
|
seeElementInDOMError(locator)
|
|
1127
1141
|
}
|
|
@@ -1151,17 +1165,12 @@ class Puppeteer extends Helper {
|
|
|
1151
1165
|
|
|
1152
1166
|
const els = await findClickable.call(this, matcher, locator)
|
|
1153
1167
|
if (context) {
|
|
1154
|
-
assertElementExists(
|
|
1155
|
-
els,
|
|
1156
|
-
locator,
|
|
1157
|
-
'Clickable element',
|
|
1158
|
-
`was not found inside element ${new Locator(context).toString()}`,
|
|
1159
|
-
)
|
|
1168
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
1160
1169
|
} else {
|
|
1161
1170
|
assertElementExists(els, locator, 'Clickable element')
|
|
1162
1171
|
}
|
|
1163
1172
|
const elem = els[0]
|
|
1164
|
-
return this.executeScript(
|
|
1173
|
+
return this.executeScript(el => {
|
|
1165
1174
|
if (document.activeElement instanceof HTMLElement) {
|
|
1166
1175
|
document.activeElement.blur()
|
|
1167
1176
|
}
|
|
@@ -1220,8 +1229,8 @@ class Puppeteer extends Helper {
|
|
|
1220
1229
|
let fileName
|
|
1221
1230
|
await this.page.setRequestInterception(true)
|
|
1222
1231
|
|
|
1223
|
-
const xRequest = await new Promise(
|
|
1224
|
-
this.page.on('request',
|
|
1232
|
+
const xRequest = await new Promise(resolve => {
|
|
1233
|
+
this.page.on('request', request => {
|
|
1225
1234
|
console.log('rq', request, customName)
|
|
1226
1235
|
const grabbedFileName = request.url().split('/')[request.url().split('/').length - 1]
|
|
1227
1236
|
const fileExtension = request.url().split('/')[request.url().split('/').length - 1].split('.')[1]
|
|
@@ -1248,7 +1257,7 @@ class Puppeteer extends Helper {
|
|
|
1248
1257
|
}
|
|
1249
1258
|
|
|
1250
1259
|
const cookies = await this.page.cookies()
|
|
1251
|
-
options.headers.Cookie = cookies.map(
|
|
1260
|
+
options.headers.Cookie = cookies.map(ck => `${ck.name}=${ck.value}`).join(';')
|
|
1252
1261
|
|
|
1253
1262
|
const response = await axios({
|
|
1254
1263
|
method: options.method,
|
|
@@ -1302,8 +1311,16 @@ class Puppeteer extends Helper {
|
|
|
1302
1311
|
*/
|
|
1303
1312
|
async checkOption(field, context = null) {
|
|
1304
1313
|
const elm = await this._locateCheckable(field, context)
|
|
1305
|
-
|
|
1306
|
-
|
|
1314
|
+
let curentlyChecked = await elm
|
|
1315
|
+
.getProperty('checked')
|
|
1316
|
+
.then(checkedProperty => checkedProperty.jsonValue())
|
|
1317
|
+
.catch(() => null)
|
|
1318
|
+
|
|
1319
|
+
if (!curentlyChecked) {
|
|
1320
|
+
const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
|
|
1321
|
+
curentlyChecked = ariaChecked === 'true'
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1307
1324
|
if (!curentlyChecked) {
|
|
1308
1325
|
await elm.click()
|
|
1309
1326
|
return this._waitForAction()
|
|
@@ -1315,8 +1332,16 @@ class Puppeteer extends Helper {
|
|
|
1315
1332
|
*/
|
|
1316
1333
|
async uncheckOption(field, context = null) {
|
|
1317
1334
|
const elm = await this._locateCheckable(field, context)
|
|
1318
|
-
|
|
1319
|
-
|
|
1335
|
+
let curentlyChecked = await elm
|
|
1336
|
+
.getProperty('checked')
|
|
1337
|
+
.then(checkedProperty => checkedProperty.jsonValue())
|
|
1338
|
+
.catch(() => null)
|
|
1339
|
+
|
|
1340
|
+
if (!curentlyChecked) {
|
|
1341
|
+
const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
|
|
1342
|
+
curentlyChecked = ariaChecked === 'true'
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1320
1345
|
if (curentlyChecked) {
|
|
1321
1346
|
await elm.click()
|
|
1322
1347
|
return this._waitForAction()
|
|
@@ -1408,12 +1433,12 @@ class Puppeteer extends Helper {
|
|
|
1408
1433
|
const els = await findVisibleFields.call(this, field)
|
|
1409
1434
|
assertElementExists(els, field, 'Field')
|
|
1410
1435
|
const el = els[0]
|
|
1411
|
-
const tag = await el.getProperty('tagName').then(
|
|
1412
|
-
const editable = await el.getProperty('contenteditable').then(
|
|
1436
|
+
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1437
|
+
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1413
1438
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1414
|
-
await this._evaluateHandeInContext(
|
|
1439
|
+
await this._evaluateHandeInContext(el => (el.value = ''), el)
|
|
1415
1440
|
} else if (editable) {
|
|
1416
|
-
await this._evaluateHandeInContext(
|
|
1441
|
+
await this._evaluateHandeInContext(el => (el.innerHTML = ''), el)
|
|
1417
1442
|
}
|
|
1418
1443
|
|
|
1419
1444
|
highlightActiveElement.call(this, el, await this._getContext())
|
|
@@ -1483,7 +1508,7 @@ class Puppeteer extends Helper {
|
|
|
1483
1508
|
const els = await findVisibleFields.call(this, select)
|
|
1484
1509
|
assertElementExists(els, select, 'Selectable field')
|
|
1485
1510
|
const el = els[0]
|
|
1486
|
-
if ((await el.getProperty('tagName').then(
|
|
1511
|
+
if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
|
|
1487
1512
|
throw new Error('Element is not <select>')
|
|
1488
1513
|
}
|
|
1489
1514
|
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
@@ -1493,15 +1518,15 @@ class Puppeteer extends Helper {
|
|
|
1493
1518
|
const opt = xpathLocator.literal(option[key])
|
|
1494
1519
|
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
|
|
1495
1520
|
if (optEl.length) {
|
|
1496
|
-
this._evaluateHandeInContext(
|
|
1521
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1497
1522
|
continue
|
|
1498
1523
|
}
|
|
1499
1524
|
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
|
|
1500
1525
|
if (optEl.length) {
|
|
1501
|
-
this._evaluateHandeInContext(
|
|
1526
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1502
1527
|
}
|
|
1503
1528
|
}
|
|
1504
|
-
await this._evaluateHandeInContext(
|
|
1529
|
+
await this._evaluateHandeInContext(element => {
|
|
1505
1530
|
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1506
1531
|
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1507
1532
|
}, el)
|
|
@@ -1515,19 +1540,11 @@ class Puppeteer extends Helper {
|
|
|
1515
1540
|
*/
|
|
1516
1541
|
async grabNumberOfVisibleElements(locator) {
|
|
1517
1542
|
let els = await this._locate(locator)
|
|
1518
|
-
els = (await Promise.all(els.map(
|
|
1543
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1519
1544
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1520
|
-
els = await Promise.all(
|
|
1521
|
-
els.map(
|
|
1522
|
-
async (el) =>
|
|
1523
|
-
(await el.evaluate(
|
|
1524
|
-
(node) =>
|
|
1525
|
-
window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none',
|
|
1526
|
-
)) && el,
|
|
1527
|
-
),
|
|
1528
|
-
)
|
|
1545
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1529
1546
|
|
|
1530
|
-
return els.filter(
|
|
1547
|
+
return els.filter(v => v).length
|
|
1531
1548
|
}
|
|
1532
1549
|
|
|
1533
1550
|
/**
|
|
@@ -1635,9 +1652,7 @@ class Puppeteer extends Helper {
|
|
|
1635
1652
|
*/
|
|
1636
1653
|
async seeNumberOfElements(locator, num) {
|
|
1637
1654
|
const elements = await this._locate(locator)
|
|
1638
|
-
return equals(
|
|
1639
|
-
`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`,
|
|
1640
|
-
).assert(elements.length, num)
|
|
1655
|
+
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
|
|
1641
1656
|
}
|
|
1642
1657
|
|
|
1643
1658
|
/**
|
|
@@ -1647,10 +1662,7 @@ class Puppeteer extends Helper {
|
|
|
1647
1662
|
*/
|
|
1648
1663
|
async seeNumberOfVisibleElements(locator, num) {
|
|
1649
1664
|
const res = await this.grabNumberOfVisibleElements(locator)
|
|
1650
|
-
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(
|
|
1651
|
-
res,
|
|
1652
|
-
num,
|
|
1653
|
-
)
|
|
1665
|
+
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
|
|
1654
1666
|
}
|
|
1655
1667
|
|
|
1656
1668
|
/**
|
|
@@ -1669,7 +1681,7 @@ class Puppeteer extends Helper {
|
|
|
1669
1681
|
*/
|
|
1670
1682
|
async seeCookie(name) {
|
|
1671
1683
|
const cookies = await this.page.cookies()
|
|
1672
|
-
empty(`cookie ${name} to be set`).negate(cookies.filter(
|
|
1684
|
+
empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
|
|
1673
1685
|
}
|
|
1674
1686
|
|
|
1675
1687
|
/**
|
|
@@ -1677,7 +1689,7 @@ class Puppeteer extends Helper {
|
|
|
1677
1689
|
*/
|
|
1678
1690
|
async dontSeeCookie(name) {
|
|
1679
1691
|
const cookies = await this.page.cookies()
|
|
1680
|
-
empty(`cookie ${name} not to be set`).assert(cookies.filter(
|
|
1692
|
+
empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
|
|
1681
1693
|
}
|
|
1682
1694
|
|
|
1683
1695
|
/**
|
|
@@ -1688,7 +1700,7 @@ class Puppeteer extends Helper {
|
|
|
1688
1700
|
async grabCookie(name) {
|
|
1689
1701
|
const cookies = await this.page.cookies()
|
|
1690
1702
|
if (!name) return cookies
|
|
1691
|
-
const cookie = cookies.filter(
|
|
1703
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1692
1704
|
if (cookie[0]) return cookie[0]
|
|
1693
1705
|
}
|
|
1694
1706
|
|
|
@@ -1708,9 +1720,9 @@ class Puppeteer extends Helper {
|
|
|
1708
1720
|
|
|
1709
1721
|
return promiseRetry(
|
|
1710
1722
|
async (retry, number) => {
|
|
1711
|
-
const _grabCookie = async
|
|
1723
|
+
const _grabCookie = async name => {
|
|
1712
1724
|
const cookies = await this.page.cookies()
|
|
1713
|
-
const cookie = cookies.filter(
|
|
1725
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1714
1726
|
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
|
|
1715
1727
|
}
|
|
1716
1728
|
|
|
@@ -1735,7 +1747,7 @@ class Puppeteer extends Helper {
|
|
|
1735
1747
|
if (!name) {
|
|
1736
1748
|
return this.page.deleteCookie.apply(this.page, cookies)
|
|
1737
1749
|
}
|
|
1738
|
-
const cookie = cookies.filter(
|
|
1750
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1739
1751
|
if (!cookie[0]) return
|
|
1740
1752
|
return this.page.deleteCookie(cookie[0])
|
|
1741
1753
|
}
|
|
@@ -1760,8 +1772,8 @@ class Puppeteer extends Helper {
|
|
|
1760
1772
|
async executeAsyncScript(...args) {
|
|
1761
1773
|
const asyncFn = function () {
|
|
1762
1774
|
const args = Array.from(arguments)
|
|
1763
|
-
const fn = eval(`(${args.shift()})`)
|
|
1764
|
-
return new Promise(
|
|
1775
|
+
const fn = eval(`(${args.shift()})`)
|
|
1776
|
+
return new Promise(done => {
|
|
1765
1777
|
args.push(done)
|
|
1766
1778
|
fn.apply(null, args)
|
|
1767
1779
|
})
|
|
@@ -1828,7 +1840,7 @@ class Puppeteer extends Helper {
|
|
|
1828
1840
|
*/
|
|
1829
1841
|
async grabHTMLFromAll(locator) {
|
|
1830
1842
|
const els = await this._locate(locator)
|
|
1831
|
-
const values = await Promise.all(els.map(
|
|
1843
|
+
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML, el)))
|
|
1832
1844
|
return values
|
|
1833
1845
|
}
|
|
1834
1846
|
|
|
@@ -1851,10 +1863,8 @@ class Puppeteer extends Helper {
|
|
|
1851
1863
|
*/
|
|
1852
1864
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1853
1865
|
const els = await this._locate(locator)
|
|
1854
|
-
const res = await Promise.all(
|
|
1855
|
-
|
|
1856
|
-
)
|
|
1857
|
-
const cssValues = res.map((props) => props[toCamelCase(cssProperty)])
|
|
1866
|
+
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)))
|
|
1867
|
+
const cssValues = res.map(props => props[toCamelCase(cssProperty)])
|
|
1858
1868
|
|
|
1859
1869
|
return cssValues
|
|
1860
1870
|
}
|
|
@@ -1897,19 +1907,16 @@ class Puppeteer extends Helper {
|
|
|
1897
1907
|
}
|
|
1898
1908
|
}
|
|
1899
1909
|
|
|
1900
|
-
const values = Object.keys(cssPropertiesCamelCase).map(
|
|
1910
|
+
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
|
|
1901
1911
|
if (!Array.isArray(props)) props = [props]
|
|
1902
1912
|
let chunked = chunkArray(props, values.length)
|
|
1903
|
-
chunked = chunked.filter(
|
|
1913
|
+
chunked = chunked.filter(val => {
|
|
1904
1914
|
for (let i = 0; i < val.length; ++i) {
|
|
1905
|
-
// eslint-disable-next-line eqeqeq
|
|
1906
1915
|
if (val[i] != values[i]) return false
|
|
1907
1916
|
}
|
|
1908
1917
|
return true
|
|
1909
1918
|
})
|
|
1910
|
-
return equals(
|
|
1911
|
-
`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`,
|
|
1912
|
-
).assert(chunked.length, elemAmount)
|
|
1919
|
+
return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
|
|
1913
1920
|
}
|
|
1914
1921
|
|
|
1915
1922
|
/**
|
|
@@ -1922,7 +1929,7 @@ class Puppeteer extends Helper {
|
|
|
1922
1929
|
|
|
1923
1930
|
const expectedAttributes = Object.entries(attributes)
|
|
1924
1931
|
|
|
1925
|
-
const valuesPromises = elements.map(async
|
|
1932
|
+
const valuesPromises = elements.map(async element => {
|
|
1926
1933
|
const elementAttributes = {}
|
|
1927
1934
|
await Promise.all(
|
|
1928
1935
|
expectedAttributes.map(async ([attribute, expectedValue]) => {
|
|
@@ -1935,7 +1942,7 @@ class Puppeteer extends Helper {
|
|
|
1935
1942
|
|
|
1936
1943
|
const actualAttributes = await Promise.all(valuesPromises)
|
|
1937
1944
|
|
|
1938
|
-
const matchingElements = actualAttributes.filter(
|
|
1945
|
+
const matchingElements = actualAttributes.filter(attrs =>
|
|
1939
1946
|
expectedAttributes.every(([attribute, expectedValue]) => {
|
|
1940
1947
|
const actualValue = attrs[attribute]
|
|
1941
1948
|
if (!actualValue) return false
|
|
@@ -1947,10 +1954,7 @@ class Puppeteer extends Helper {
|
|
|
1947
1954
|
const elementsCount = elements.length
|
|
1948
1955
|
const matchingCount = matchingElements.length
|
|
1949
1956
|
|
|
1950
|
-
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(
|
|
1951
|
-
matchingCount,
|
|
1952
|
-
elementsCount,
|
|
1953
|
-
)
|
|
1957
|
+
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(matchingCount, elementsCount)
|
|
1954
1958
|
}
|
|
1955
1959
|
|
|
1956
1960
|
/**
|
|
@@ -2080,7 +2084,7 @@ class Puppeteer extends Helper {
|
|
|
2080
2084
|
* {{> wait }}
|
|
2081
2085
|
*/
|
|
2082
2086
|
async wait(sec) {
|
|
2083
|
-
return new Promise(
|
|
2087
|
+
return new Promise(done => {
|
|
2084
2088
|
setTimeout(done, sec * 1000)
|
|
2085
2089
|
})
|
|
2086
2090
|
}
|
|
@@ -2100,20 +2104,18 @@ class Puppeteer extends Helper {
|
|
|
2100
2104
|
if (!els || els.length === 0) {
|
|
2101
2105
|
return false
|
|
2102
2106
|
}
|
|
2103
|
-
return Array.prototype.filter.call(els,
|
|
2107
|
+
return Array.prototype.filter.call(els, el => !el.disabled).length > 0
|
|
2104
2108
|
}
|
|
2105
2109
|
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value)
|
|
2106
2110
|
} else {
|
|
2107
2111
|
const enabledFn = function (locator, $XPath) {
|
|
2108
|
-
eval($XPath)
|
|
2109
|
-
return $XPath(null, locator).filter(
|
|
2112
|
+
eval($XPath)
|
|
2113
|
+
return $XPath(null, locator).filter(el => !el.disabled).length > 0
|
|
2110
2114
|
}
|
|
2111
2115
|
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2112
2116
|
}
|
|
2113
|
-
return waiter.catch(
|
|
2114
|
-
throw new Error(
|
|
2115
|
-
`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2116
|
-
)
|
|
2117
|
+
return waiter.catch(err => {
|
|
2118
|
+
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2117
2119
|
})
|
|
2118
2120
|
}
|
|
2119
2121
|
|
|
@@ -2132,21 +2134,19 @@ class Puppeteer extends Helper {
|
|
|
2132
2134
|
if (!els || els.length === 0) {
|
|
2133
2135
|
return false
|
|
2134
2136
|
}
|
|
2135
|
-
return Array.prototype.filter.call(els,
|
|
2137
|
+
return Array.prototype.filter.call(els, el => (el.value.toString() || '').indexOf(value) !== -1).length > 0
|
|
2136
2138
|
}
|
|
2137
2139
|
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value)
|
|
2138
2140
|
} else {
|
|
2139
2141
|
const valueFn = function (locator, $XPath, value) {
|
|
2140
|
-
eval($XPath)
|
|
2141
|
-
return $XPath(null, locator).filter(
|
|
2142
|
+
eval($XPath)
|
|
2143
|
+
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2142
2144
|
}
|
|
2143
2145
|
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value)
|
|
2144
2146
|
}
|
|
2145
|
-
return waiter.catch(
|
|
2147
|
+
return waiter.catch(err => {
|
|
2146
2148
|
const loc = locator.toString()
|
|
2147
|
-
throw new Error(
|
|
2148
|
-
`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2149
|
-
)
|
|
2149
|
+
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}`)
|
|
2150
2150
|
})
|
|
2151
2151
|
}
|
|
2152
2152
|
|
|
@@ -2165,20 +2165,18 @@ class Puppeteer extends Helper {
|
|
|
2165
2165
|
if (!els || els.length === 0) {
|
|
2166
2166
|
return false
|
|
2167
2167
|
}
|
|
2168
|
-
return Array.prototype.filter.call(els,
|
|
2168
|
+
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
|
|
2169
2169
|
}
|
|
2170
2170
|
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num)
|
|
2171
2171
|
} else {
|
|
2172
2172
|
const visibleFn = function (locator, $XPath, num) {
|
|
2173
|
-
eval($XPath)
|
|
2174
|
-
return $XPath(null, locator).filter(
|
|
2173
|
+
eval($XPath)
|
|
2174
|
+
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
|
|
2175
2175
|
}
|
|
2176
2176
|
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num)
|
|
2177
2177
|
}
|
|
2178
|
-
return waiter.catch(
|
|
2179
|
-
throw new Error(
|
|
2180
|
-
`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2181
|
-
)
|
|
2178
|
+
return waiter.catch(err => {
|
|
2179
|
+
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2182
2180
|
})
|
|
2183
2181
|
}
|
|
2184
2182
|
|
|
@@ -2189,11 +2187,10 @@ class Puppeteer extends Helper {
|
|
|
2189
2187
|
const els = await this._locate(locator)
|
|
2190
2188
|
assertElementExists(els, locator)
|
|
2191
2189
|
|
|
2192
|
-
return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
)
|
|
2190
|
+
return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async e => {
|
|
2191
|
+
const errorMessage = e?.message || String(e)
|
|
2192
|
+
if (/Waiting failed/i.test(errorMessage) || /failed: timeout/i.test(errorMessage)) {
|
|
2193
|
+
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
|
|
2197
2194
|
} else {
|
|
2198
2195
|
throw e
|
|
2199
2196
|
}
|
|
@@ -2215,10 +2212,8 @@ class Puppeteer extends Helper {
|
|
|
2215
2212
|
} else {
|
|
2216
2213
|
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout })
|
|
2217
2214
|
}
|
|
2218
|
-
return waiter.catch(
|
|
2219
|
-
throw new Error(
|
|
2220
|
-
`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2221
|
-
)
|
|
2215
|
+
return waiter.catch(err => {
|
|
2216
|
+
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2222
2217
|
})
|
|
2223
2218
|
}
|
|
2224
2219
|
|
|
@@ -2238,10 +2233,8 @@ class Puppeteer extends Helper {
|
|
|
2238
2233
|
} else {
|
|
2239
2234
|
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true })
|
|
2240
2235
|
}
|
|
2241
|
-
return waiter.catch(
|
|
2242
|
-
throw new Error(
|
|
2243
|
-
`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2244
|
-
)
|
|
2236
|
+
return waiter.catch(err => {
|
|
2237
|
+
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2245
2238
|
})
|
|
2246
2239
|
}
|
|
2247
2240
|
|
|
@@ -2259,7 +2252,7 @@ class Puppeteer extends Helper {
|
|
|
2259
2252
|
} else {
|
|
2260
2253
|
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2261
2254
|
}
|
|
2262
|
-
return waiter.catch(
|
|
2255
|
+
return waiter.catch(err => {
|
|
2263
2256
|
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2264
2257
|
})
|
|
2265
2258
|
}
|
|
@@ -2277,10 +2270,8 @@ class Puppeteer extends Helper {
|
|
|
2277
2270
|
} else {
|
|
2278
2271
|
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2279
2272
|
}
|
|
2280
|
-
return waiter.catch(
|
|
2281
|
-
throw new Error(
|
|
2282
|
-
`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`,
|
|
2283
|
-
)
|
|
2273
|
+
return waiter.catch(err => {
|
|
2274
|
+
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2284
2275
|
})
|
|
2285
2276
|
}
|
|
2286
2277
|
|
|
@@ -2317,14 +2308,14 @@ class Puppeteer extends Helper {
|
|
|
2317
2308
|
|
|
2318
2309
|
return this.page
|
|
2319
2310
|
.waitForFunction(
|
|
2320
|
-
|
|
2311
|
+
urlPart => {
|
|
2321
2312
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2322
2313
|
return currUrl.indexOf(urlPart) > -1
|
|
2323
2314
|
},
|
|
2324
2315
|
{ timeout: waitTimeout },
|
|
2325
2316
|
urlPart,
|
|
2326
2317
|
)
|
|
2327
|
-
.catch(async
|
|
2318
|
+
.catch(async e => {
|
|
2328
2319
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2329
2320
|
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2330
2321
|
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
@@ -2347,14 +2338,14 @@ class Puppeteer extends Helper {
|
|
|
2347
2338
|
|
|
2348
2339
|
return this.page
|
|
2349
2340
|
.waitForFunction(
|
|
2350
|
-
|
|
2341
|
+
urlPart => {
|
|
2351
2342
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2352
2343
|
return currUrl.indexOf(urlPart) > -1
|
|
2353
2344
|
},
|
|
2354
2345
|
{ timeout: waitTimeout },
|
|
2355
2346
|
urlPart,
|
|
2356
2347
|
)
|
|
2357
|
-
.catch(async
|
|
2348
|
+
.catch(async e => {
|
|
2358
2349
|
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2359
2350
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2360
2351
|
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
|
|
@@ -2391,7 +2382,7 @@ class Puppeteer extends Helper {
|
|
|
2391
2382
|
if (locator.isXPath()) {
|
|
2392
2383
|
waiter = contextObject.waitForFunction(
|
|
2393
2384
|
(locator, text, $XPath) => {
|
|
2394
|
-
eval($XPath)
|
|
2385
|
+
eval($XPath)
|
|
2395
2386
|
const el = $XPath(null, locator)
|
|
2396
2387
|
if (!el.length) return false
|
|
2397
2388
|
return el[0].innerText.indexOf(text) > -1
|
|
@@ -2403,14 +2394,10 @@ class Puppeteer extends Helper {
|
|
|
2403
2394
|
)
|
|
2404
2395
|
}
|
|
2405
2396
|
} else {
|
|
2406
|
-
waiter = contextObject.waitForFunction(
|
|
2407
|
-
(text) => document.body && document.body.innerText.indexOf(text) > -1,
|
|
2408
|
-
{ timeout: waitTimeout },
|
|
2409
|
-
text,
|
|
2410
|
-
)
|
|
2397
|
+
waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text)
|
|
2411
2398
|
}
|
|
2412
2399
|
|
|
2413
|
-
return waiter.catch(
|
|
2400
|
+
return waiter.catch(err => {
|
|
2414
2401
|
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2415
2402
|
})
|
|
2416
2403
|
}
|
|
@@ -2543,12 +2530,12 @@ class Puppeteer extends Helper {
|
|
|
2543
2530
|
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value)
|
|
2544
2531
|
} else {
|
|
2545
2532
|
const visibleFn = function (locator, $XPath) {
|
|
2546
|
-
eval($XPath)
|
|
2533
|
+
eval($XPath)
|
|
2547
2534
|
return $XPath(null, locator).length === 0
|
|
2548
2535
|
}
|
|
2549
2536
|
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2550
2537
|
}
|
|
2551
|
-
return waiter.catch(
|
|
2538
|
+
return waiter.catch(err => {
|
|
2552
2539
|
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2553
2540
|
})
|
|
2554
2541
|
}
|
|
@@ -2589,7 +2576,7 @@ class Puppeteer extends Helper {
|
|
|
2589
2576
|
async mockRoute(url, handler) {
|
|
2590
2577
|
await this.page.setRequestInterception(true)
|
|
2591
2578
|
|
|
2592
|
-
this.page.on('request',
|
|
2579
|
+
this.page.on('request', interceptedRequest => {
|
|
2593
2580
|
if (interceptedRequest.url().match(url)) {
|
|
2594
2581
|
// @ts-ignore
|
|
2595
2582
|
handler(interceptedRequest)
|
|
@@ -2614,7 +2601,7 @@ class Puppeteer extends Helper {
|
|
|
2614
2601
|
this.page.off('request')
|
|
2615
2602
|
|
|
2616
2603
|
// Resume normal request handling for the given URL
|
|
2617
|
-
this.page.on('request',
|
|
2604
|
+
this.page.on('request', interceptedRequest => {
|
|
2618
2605
|
if (interceptedRequest.url().includes(url)) {
|
|
2619
2606
|
interceptedRequest.continue()
|
|
2620
2607
|
} else {
|
|
@@ -2650,7 +2637,7 @@ class Puppeteer extends Helper {
|
|
|
2650
2637
|
|
|
2651
2638
|
await this.page.setRequestInterception(true)
|
|
2652
2639
|
|
|
2653
|
-
this.page.on('request',
|
|
2640
|
+
this.page.on('request', request => {
|
|
2654
2641
|
const information = {
|
|
2655
2642
|
url: request.url(),
|
|
2656
2643
|
method: request.method(),
|
|
@@ -2711,15 +2698,15 @@ class Puppeteer extends Helper {
|
|
|
2711
2698
|
await this.cdpSession.send('Network.enable')
|
|
2712
2699
|
await this.cdpSession.send('Page.enable')
|
|
2713
2700
|
|
|
2714
|
-
this.cdpSession.on('Network.webSocketFrameReceived',
|
|
2701
|
+
this.cdpSession.on('Network.webSocketFrameReceived', payload => {
|
|
2715
2702
|
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
|
|
2716
2703
|
})
|
|
2717
2704
|
|
|
2718
|
-
this.cdpSession.on('Network.webSocketFrameSent',
|
|
2705
|
+
this.cdpSession.on('Network.webSocketFrameSent', payload => {
|
|
2719
2706
|
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
|
|
2720
2707
|
})
|
|
2721
2708
|
|
|
2722
|
-
this.cdpSession.on('Network.webSocketFrameError',
|
|
2709
|
+
this.cdpSession.on('Network.webSocketFrameError', payload => {
|
|
2723
2710
|
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
|
|
2724
2711
|
})
|
|
2725
2712
|
}
|
|
@@ -2743,9 +2730,7 @@ class Puppeteer extends Helper {
|
|
|
2743
2730
|
grabWebSocketMessages() {
|
|
2744
2731
|
if (!this.recordingWebSocketMessages) {
|
|
2745
2732
|
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
2746
|
-
throw new Error(
|
|
2747
|
-
'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.',
|
|
2748
|
-
)
|
|
2733
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
|
|
2749
2734
|
}
|
|
2750
2735
|
}
|
|
2751
2736
|
return this.webSocketMessages
|
|
@@ -2775,17 +2760,27 @@ class Puppeteer extends Helper {
|
|
|
2775
2760
|
}
|
|
2776
2761
|
}
|
|
2777
2762
|
|
|
2778
|
-
module.exports = Puppeteer
|
|
2779
|
-
|
|
2780
2763
|
async function findElements(matcher, locator) {
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
if (
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2764
|
+
const matchedLocator = new Locator(locator, 'css')
|
|
2765
|
+
|
|
2766
|
+
if (matchedLocator.type === 'react') return findReactElements.call(this, matchedLocator)
|
|
2767
|
+
if (matchedLocator.isRole()) return findByRole.call(this, matcher, matchedLocator)
|
|
2768
|
+
|
|
2769
|
+
if (!matchedLocator.isXPath()) return matcher.$$(matchedLocator.simplify())
|
|
2770
|
+
|
|
2771
|
+
// Handle backward compatibility for different Puppeteer versions
|
|
2772
|
+
// Puppeteer >= 19.4.0 uses xpath/ syntax, older versions use $x
|
|
2773
|
+
try {
|
|
2774
|
+
// Try the new xpath syntax first (for Puppeteer >= 19.4.0)
|
|
2775
|
+
return await matcher.$$(`xpath/${matchedLocator.value}`)
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
// Fall back to the old $x method for older Puppeteer versions
|
|
2778
|
+
if (matcher.$x && typeof matcher.$x === 'function') {
|
|
2779
|
+
return await matcher.$x(matchedLocator.value)
|
|
2780
|
+
}
|
|
2781
|
+
// If both methods fail, re-throw the original error
|
|
2782
|
+
throw error
|
|
2783
|
+
}
|
|
2789
2784
|
}
|
|
2790
2785
|
|
|
2791
2786
|
async function proceedClick(locator, context = null, options = {}) {
|
|
@@ -2797,12 +2792,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2797
2792
|
}
|
|
2798
2793
|
const els = await findClickable.call(this, matcher, locator)
|
|
2799
2794
|
if (context) {
|
|
2800
|
-
assertElementExists(
|
|
2801
|
-
els,
|
|
2802
|
-
locator,
|
|
2803
|
-
'Clickable element',
|
|
2804
|
-
`was not found inside element ${new Locator(context).toString()}`,
|
|
2805
|
-
)
|
|
2795
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
2806
2796
|
} else {
|
|
2807
2797
|
assertElementExists(els, locator, 'Clickable element')
|
|
2808
2798
|
}
|
|
@@ -2820,12 +2810,12 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2820
2810
|
}
|
|
2821
2811
|
|
|
2822
2812
|
async function findClickable(matcher, locator) {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (!
|
|
2813
|
+
const matchedLocator = new Locator(locator)
|
|
2814
|
+
|
|
2815
|
+
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
|
|
2826
2816
|
|
|
2827
2817
|
let els
|
|
2828
|
-
const literal = xpathLocator.literal(
|
|
2818
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2829
2819
|
|
|
2830
2820
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
2831
2821
|
if (els.length) return els
|
|
@@ -2840,7 +2830,15 @@ async function findClickable(matcher, locator) {
|
|
|
2840
2830
|
// Do nothing
|
|
2841
2831
|
}
|
|
2842
2832
|
|
|
2843
|
-
|
|
2833
|
+
// Try ARIA selector for accessible name
|
|
2834
|
+
try {
|
|
2835
|
+
els = await matcher.$$(`::-p-aria(${matchedLocator.value})`)
|
|
2836
|
+
if (els.length) return els
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
// ARIA selector not supported or failed
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
|
|
2844
2842
|
}
|
|
2845
2843
|
|
|
2846
2844
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -2854,23 +2852,25 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
2854
2852
|
el = await this.context.$('body')
|
|
2855
2853
|
}
|
|
2856
2854
|
|
|
2857
|
-
allText = [await el.getProperty('innerText').then(
|
|
2855
|
+
allText = [await el.getProperty('innerText').then(p => p.jsonValue())]
|
|
2858
2856
|
description = 'web application'
|
|
2859
2857
|
} else {
|
|
2860
2858
|
const locator = new Locator(context, 'css')
|
|
2861
2859
|
description = `element ${locator.toString()}`
|
|
2862
2860
|
const els = await this._locate(locator)
|
|
2863
2861
|
assertElementExists(els, locator.toString())
|
|
2864
|
-
allText = await Promise.all(els.map(
|
|
2862
|
+
allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
if (store?.currentStep?.opts?.ignoreCase === true) {
|
|
2866
|
+
text = text.toLowerCase()
|
|
2867
|
+
allText = allText.map(elText => elText.toLowerCase())
|
|
2865
2868
|
}
|
|
2866
2869
|
|
|
2867
2870
|
if (strict) {
|
|
2868
|
-
return allText.map(
|
|
2871
|
+
return allText.map(elText => equals(description)[assertType](text, elText))
|
|
2869
2872
|
}
|
|
2870
|
-
return stringIncludes(description)[assertType](
|
|
2871
|
-
normalizeSpacesInString(text),
|
|
2872
|
-
normalizeSpacesInString(allText.join(' | ')),
|
|
2873
|
-
)
|
|
2873
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
|
|
2874
2874
|
}
|
|
2875
2875
|
|
|
2876
2876
|
async function findCheckable(locator, context) {
|
|
@@ -2882,10 +2882,10 @@ async function findCheckable(locator, context) {
|
|
|
2882
2882
|
|
|
2883
2883
|
const matchedLocator = new Locator(locator)
|
|
2884
2884
|
if (!matchedLocator.isFuzzy()) {
|
|
2885
|
-
return findElements.call(this, contextEl, matchedLocator
|
|
2885
|
+
return findElements.call(this, contextEl, matchedLocator)
|
|
2886
2886
|
}
|
|
2887
2887
|
|
|
2888
|
-
const literal = xpathLocator.literal(
|
|
2888
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2889
2889
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
2890
2890
|
if (els.length) {
|
|
2891
2891
|
return els
|
|
@@ -2894,21 +2894,45 @@ async function findCheckable(locator, context) {
|
|
|
2894
2894
|
if (els.length) {
|
|
2895
2895
|
return els
|
|
2896
2896
|
}
|
|
2897
|
-
|
|
2897
|
+
|
|
2898
|
+
// Try ARIA selector for accessible name
|
|
2899
|
+
try {
|
|
2900
|
+
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
|
|
2901
|
+
if (els.length) return els
|
|
2902
|
+
} catch (err) {
|
|
2903
|
+
// ARIA selector not supported or failed
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
return findElements.call(this, contextEl, matchedLocator.value)
|
|
2898
2907
|
}
|
|
2899
2908
|
|
|
2900
2909
|
async function proceedIsChecked(assertType, option) {
|
|
2901
2910
|
let els = await findCheckable.call(this, option)
|
|
2902
2911
|
assertElementExists(els, option, 'Checkable')
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2912
|
+
|
|
2913
|
+
const checkedStates = await Promise.all(
|
|
2914
|
+
els.map(async el => {
|
|
2915
|
+
const checked = await el
|
|
2916
|
+
.getProperty('checked')
|
|
2917
|
+
.then(p => p.jsonValue())
|
|
2918
|
+
.catch(() => null)
|
|
2919
|
+
|
|
2920
|
+
if (checked) {
|
|
2921
|
+
return checked
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
const ariaChecked = await el.evaluate(el => el.getAttribute('aria-checked'))
|
|
2925
|
+
return ariaChecked === 'true'
|
|
2926
|
+
}),
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
const selected = checkedStates.reduce((prev, cur) => prev || cur)
|
|
2906
2930
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
2907
2931
|
}
|
|
2908
2932
|
|
|
2909
2933
|
async function findVisibleFields(locator) {
|
|
2910
2934
|
const els = await findFields.call(this, locator)
|
|
2911
|
-
const visible = await Promise.all(els.map(
|
|
2935
|
+
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
2912
2936
|
return els.filter((el, index) => visible[index])
|
|
2913
2937
|
}
|
|
2914
2938
|
|
|
@@ -2917,7 +2941,7 @@ async function findFields(locator) {
|
|
|
2917
2941
|
if (!matchedLocator.isFuzzy()) {
|
|
2918
2942
|
return this._locate(matchedLocator)
|
|
2919
2943
|
}
|
|
2920
|
-
const literal = xpathLocator.literal(
|
|
2944
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2921
2945
|
|
|
2922
2946
|
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
|
|
2923
2947
|
if (els.length) {
|
|
@@ -2932,7 +2956,17 @@ async function findFields(locator) {
|
|
|
2932
2956
|
if (els.length) {
|
|
2933
2957
|
return els
|
|
2934
2958
|
}
|
|
2935
|
-
|
|
2959
|
+
|
|
2960
|
+
// Try ARIA selector for accessible name
|
|
2961
|
+
try {
|
|
2962
|
+
const page = await this.context
|
|
2963
|
+
els = await page.$$(`::-p-aria(${matchedLocator.value})`)
|
|
2964
|
+
if (els.length) return els
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
// ARIA selector not supported or failed
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
return this._locate({ css: matchedLocator.value })
|
|
2936
2970
|
}
|
|
2937
2971
|
|
|
2938
2972
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
@@ -2961,15 +2995,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2961
2995
|
const els = await findVisibleFields.call(this, field)
|
|
2962
2996
|
assertElementExists(els, field, 'Field')
|
|
2963
2997
|
const el = els[0]
|
|
2964
|
-
const tag = await el.getProperty('tagName').then(
|
|
2965
|
-
const fieldType = await el.getProperty('type').then(
|
|
2998
|
+
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
2999
|
+
const fieldType = await el.getProperty('type').then(el => el.jsonValue())
|
|
2966
3000
|
|
|
2967
|
-
const proceedMultiple = async
|
|
3001
|
+
const proceedMultiple = async elements => {
|
|
2968
3002
|
const fields = Array.isArray(elements) ? elements : [elements]
|
|
2969
3003
|
|
|
2970
3004
|
const elementValues = []
|
|
2971
3005
|
for (const element of fields) {
|
|
2972
|
-
elementValues.push(await element.getProperty('value').then(
|
|
3006
|
+
elementValues.push(await element.getProperty('value').then(el => el.jsonValue()))
|
|
2973
3007
|
}
|
|
2974
3008
|
|
|
2975
3009
|
if (typeof value === 'boolean') {
|
|
@@ -2978,7 +3012,7 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2978
3012
|
if (assertType === 'assert') {
|
|
2979
3013
|
equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
|
|
2980
3014
|
}
|
|
2981
|
-
elementValues.forEach(
|
|
3015
|
+
elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
|
|
2982
3016
|
}
|
|
2983
3017
|
}
|
|
2984
3018
|
|
|
@@ -3006,19 +3040,30 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3006
3040
|
}
|
|
3007
3041
|
return proceedMultiple(els[0])
|
|
3008
3042
|
}
|
|
3009
|
-
|
|
3043
|
+
|
|
3044
|
+
let fieldVal = await el.getProperty('value').then(el => el.jsonValue())
|
|
3045
|
+
|
|
3046
|
+
if (fieldVal === undefined || fieldVal === null) {
|
|
3047
|
+
fieldVal = await el.evaluate(el => el.textContent || el.innerText)
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3010
3050
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
|
|
3011
3051
|
}
|
|
3012
3052
|
|
|
3013
3053
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
3014
3054
|
const matches = []
|
|
3015
3055
|
for (const element of elements) {
|
|
3016
|
-
|
|
3056
|
+
let val = await element.getProperty('value').then(el => el.jsonValue())
|
|
3057
|
+
|
|
3058
|
+
if (val === undefined || val === null) {
|
|
3059
|
+
val = await element.evaluate(el => el.textContent || el.innerText)
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3017
3062
|
let isSelected = true
|
|
3018
3063
|
if (onlySelected) {
|
|
3019
3064
|
isSelected = await elementSelected(element)
|
|
3020
3065
|
}
|
|
3021
|
-
if ((value == null || val.indexOf(value) > -1) && isSelected) {
|
|
3066
|
+
if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
|
|
3022
3067
|
matches.push(element)
|
|
3023
3068
|
}
|
|
3024
3069
|
}
|
|
@@ -3037,12 +3082,12 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
3037
3082
|
}
|
|
3038
3083
|
|
|
3039
3084
|
async function elementSelected(element) {
|
|
3040
|
-
const type = await element.getProperty('type').then(
|
|
3085
|
+
const type = await element.getProperty('type').then(el => el.jsonValue())
|
|
3041
3086
|
|
|
3042
3087
|
if (type === 'checkbox' || type === 'radio') {
|
|
3043
|
-
return element.getProperty('checked').then(
|
|
3088
|
+
return element.getProperty('checked').then(el => el.jsonValue())
|
|
3044
3089
|
}
|
|
3045
|
-
return element.getProperty('selected').then(
|
|
3090
|
+
return element.getProperty('selected').then(el => el.jsonValue())
|
|
3046
3091
|
}
|
|
3047
3092
|
|
|
3048
3093
|
function isFrameLocator(locator) {
|
|
@@ -3077,9 +3122,9 @@ async function targetCreatedHandler(page) {
|
|
|
3077
3122
|
page
|
|
3078
3123
|
.$('body')
|
|
3079
3124
|
.catch(() => null)
|
|
3080
|
-
.then(
|
|
3125
|
+
.then(context => (this.context = context))
|
|
3081
3126
|
})
|
|
3082
|
-
page.on('console',
|
|
3127
|
+
page.on('console', msg => {
|
|
3083
3128
|
this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '))
|
|
3084
3129
|
consoleLogStore.add(msg)
|
|
3085
3130
|
})
|
|
@@ -3180,13 +3225,18 @@ function _waitForElement(locator, options) {
|
|
|
3180
3225
|
}
|
|
3181
3226
|
}
|
|
3182
3227
|
|
|
3183
|
-
async function findReactElements(locator
|
|
3228
|
+
async function findReactElements(locator) {
|
|
3229
|
+
const resolved = toLocatorConfig(locator, 'react')
|
|
3230
|
+
|
|
3231
|
+
// Use createRequire to access require.resolve in ESM
|
|
3232
|
+
const { createRequire } = await import('module')
|
|
3233
|
+
const require = createRequire(import.meta.url)
|
|
3184
3234
|
const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
|
|
3185
3235
|
await this.page.evaluate(resqScript.toString())
|
|
3186
3236
|
|
|
3187
3237
|
await this.page.evaluate(() => window.resq.waitToLoadReact())
|
|
3188
3238
|
const arrayHandle = await this.page.evaluateHandle(
|
|
3189
|
-
|
|
3239
|
+
obj => {
|
|
3190
3240
|
const { selector, props, state } = obj
|
|
3191
3241
|
let elements = window.resq.resq$$(selector)
|
|
3192
3242
|
if (Object.keys(props).length) {
|
|
@@ -3205,7 +3255,7 @@ async function findReactElements(locator, props = {}, state = {}) {
|
|
|
3205
3255
|
// [[div, div], [div, div]] => [div, div, div, div]
|
|
3206
3256
|
let nodes = []
|
|
3207
3257
|
|
|
3208
|
-
elements.forEach(
|
|
3258
|
+
elements.forEach(element => {
|
|
3209
3259
|
let { node, isFragment } = element
|
|
3210
3260
|
|
|
3211
3261
|
if (!node) {
|
|
@@ -3223,9 +3273,9 @@ async function findReactElements(locator, props = {}, state = {}) {
|
|
|
3223
3273
|
return [...nodes]
|
|
3224
3274
|
},
|
|
3225
3275
|
{
|
|
3226
|
-
selector:
|
|
3227
|
-
props:
|
|
3228
|
-
state:
|
|
3276
|
+
selector: resolved.react,
|
|
3277
|
+
props: resolved.props || {},
|
|
3278
|
+
state: resolved.state || {},
|
|
3229
3279
|
},
|
|
3230
3280
|
)
|
|
3231
3281
|
|
|
@@ -3241,3 +3291,54 @@ async function findReactElements(locator, props = {}, state = {}) {
|
|
|
3241
3291
|
await arrayHandle.dispose()
|
|
3242
3292
|
return result
|
|
3243
3293
|
}
|
|
3294
|
+
|
|
3295
|
+
async function findByRole(matcher, locator) {
|
|
3296
|
+
const resolved = toLocatorConfig(locator, 'role')
|
|
3297
|
+
const roleSelector = buildRoleSelector(resolved)
|
|
3298
|
+
|
|
3299
|
+
if (!resolved.text && !resolved.name) {
|
|
3300
|
+
return matcher.$$(roleSelector)
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
const allElements = await matcher.$$(roleSelector)
|
|
3304
|
+
const filtered = []
|
|
3305
|
+
const accessibleName = resolved.text ?? resolved.name
|
|
3306
|
+
const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
|
|
3307
|
+
|
|
3308
|
+
for (const el of allElements) {
|
|
3309
|
+
const texts = await el.evaluate(e => {
|
|
3310
|
+
const ariaLabel = e.hasAttribute('aria-label') ? e.getAttribute('aria-label') : ''
|
|
3311
|
+
const labelText = e.id ? document.querySelector(`label[for="${e.id}"]`)?.textContent.trim() || '' : ''
|
|
3312
|
+
const placeholder = e.getAttribute('placeholder') || ''
|
|
3313
|
+
const innerText = e.innerText ? e.innerText.trim() : ''
|
|
3314
|
+
return [ariaLabel || labelText, placeholder, innerText]
|
|
3315
|
+
})
|
|
3316
|
+
|
|
3317
|
+
if (texts.some(text => matcherFn(text))) filtered.push(el)
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
return filtered
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function toLocatorConfig(locator, key) {
|
|
3324
|
+
const matchedLocator = new Locator(locator, key)
|
|
3325
|
+
if (matchedLocator.locator) return matchedLocator.locator
|
|
3326
|
+
return { [key]: matchedLocator.value }
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function buildRoleSelector(resolved) {
|
|
3330
|
+
return `::-p-aria([role="${resolved.role}"])`
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function createRoleTextMatcher(expected, exactMatch) {
|
|
3334
|
+
if (expected instanceof RegExp) {
|
|
3335
|
+
return value => expected.test(value || '')
|
|
3336
|
+
}
|
|
3337
|
+
const target = String(expected)
|
|
3338
|
+
if (exactMatch) {
|
|
3339
|
+
return value => value === target
|
|
3340
|
+
}
|
|
3341
|
+
return value => typeof value === 'string' && value.includes(target)
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
export { Puppeteer as default }
|